1/* 2 * site.js â All site JavaScript in one file. 3 * Replaces: webflow.js, jQuery, lottie, menu.js, carousel.js, anchor-scroll.js, packages-loader.js 4 */ 5(function () { 6 'use strict'; 7 8 // ââ Navbar ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 9 function initNavbar() { 10 var nav = document.querySelector('.w-nav'); 11 if (!nav) return; 12 var btn = nav.querySelector('.w-nav-button'); 13 var menu = nav.querySelector('.w-nav-menu'); 14 var overlay = nav.querySelector('.w-nav-overlay'); 15 if (!btn || !menu) return; 16 17 var isOpen = false; 18 var collapse = nav.getAttribute('data-collapse') || 'medium'; 19 var bp = collapse === 'medium' ? 991 : collapse === 'small' ? 767 : 479; 20 21 function open() { 22 isOpen = true; 23 btn.classList.add('w--open'); 24 btn.setAttribute('aria-expanded', 'true'); 25 menu.setAttribute('data-nav-menu-open', ''); 26 if (overlay) { 27 overlay.style.display = 'block'; 28 overlay.style.height = '0'; 29 overlay.appendChild(menu); 30 var h = menu.scrollHeight; 31 overlay.style.transition = 'height 350ms ease'; 32 overlay.offsetHeight; 33 overlay.style.height = h + 'px'; 34 } 35 document.body.style.overflow = 'hidden'; 36 } 37 38 function close() { 39 isOpen = false; 40 btn.classList.remove('w--open'); 41 btn.setAttribute('aria-expanded', 'false'); 42 menu.removeAttribute('data-nav-menu-open'); 43 document.body.style.overflow = ''; 44 if (overlay) { 45 overlay.style.height = '0'; 46 setTimeout(function () { 47 overlay.style.display = 'none'; 48 var headerWrapper = nav.querySelector('.header-wrapper'); 49 if (headerWrapper) { 50 headerWrapper.insertBefore(menu, overlay); 51 } 52 }, 350); 53 } 54 } 55 56 btn.addEventListener('click', function () { 57 isOpen ? close() : open(); 58 }); 59 60 menu.addEventListener('click', function (e) { 61 if (e.target.closest('a') && isOpen) { 62 setTimeout(close, 150); 63 } 64 }); 65 66 window.addEventListener('resize', function () { 67 if (window.innerWidth > bp && isOpen) close(); 68 }); 69 } 70 71 // ââ Hamburger (CSS-based, replaces lottie) âââââââââââââââââââââââââ 72 function initHamburger() { 73 var icon = document.querySelector('.hamburger-menu-icon'); 74 if (!icon) return; 75 icon.innerHTML = '<span></span><span></span><span></span>'; 76 77 var style = document.createElement('style'); 78 style.textContent = 79 '.hamburger-menu-icon{width:22px;height:18px;position:relative;display:flex;flex-direction:column;justify-content:space-between;cursor:pointer}' + 80 '.hamburger-menu-icon span{display:block;height:2px;width:100%;background:#fff;border-radius:2px;transition:transform .3s ease,opacity .3s ease}' + 81 '.w-nav-button.w--open .hamburger-menu-icon span:nth-child(1){transform:translateY(8px) rotate(45deg)}' + 82 '.w-nav-button.w--open .hamburger-menu-icon span:nth-child(2){opacity:0}' + 83 '.w-nav-button.w--open .hamburger-menu-icon span:nth-child(3){transform:translateY(-8px) rotate(-45deg)}'; 84 document.head.appendChild(style); 85 } 86 87 // ââ Dropdowns ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 88 function initDropdowns() { 89 document.querySelectorAll('.w-dropdown').forEach(function (dd) { 90 var toggle = dd.querySelector('.w-dropdown-toggle'); 91 var list = dd.querySelector('.w-dropdown-list'); 92 if (!toggle || !list) return; 93 94 var isFaq = list.classList.contains('faq-dropdown-list'); 95 96 toggle.addEventListener('click', function (e) { 97 e.preventDefault(); 98 e.stopPropagation(); 99 var wasOpen = list.classList.contains('w--open'); 100 closeAllDropdowns(); 101 if (!wasOpen) { 102 list.classList.add('w--open'); 103 if (isFaq) list.style.height = list.scrollHeight + 'px'; 104 } 105 }); 106 }); 107 108 document.addEventListener('click', function (e) { 109 if (!e.target.closest('.w-dropdown')) closeAllDropdowns(); 110 }); 111 112 document.addEventListener('keydown', function (e) { 113 if (e.key === 'Escape') closeAllDropdowns(); 114 }); 115 } 116 117 function closeAllDropdowns() { 118 document.querySelectorAll('.w-dropdown-list.w--open').forEach(function (l) { 119 l.classList.remove('w--open'); 120 if (l.classList.contains('faq-dropdown-list')) l.style.height = '0'; 121 }); 122 } 123 124 // ââ Slider âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 125 function initSliders() { 126 document.querySelectorAll('.w-slider').forEach(function (slider) { 127 var mask = slider.querySelector('.w-slider-mask'); 128 var slides = mask ? mask.querySelectorAll('.w-slide') : []; 129 var leftArrow = slider.querySelector('.w-slider-arrow-left'); 130 var rightArrow = slider.querySelector('.w-slider-arrow-right'); 131 var nav = slider.querySelector('.w-slider-nav'); 132 if (!mask || !slides.length) return; 133 134 var current = 0; 135 var total = slides.length; 136 var infinite = slider.getAttribute('data-infinite') !== 'false'; 137 var autoDelay = parseInt(slider.getAttribute('data-delay')) || 2000; 138 var autoplay = slider.getAttribute('data-autoplay') === 'true'; 139 var autoTimer = null; 140 141 function styleSlide(slide, active) { 142 var card = slide.querySelector('.slider-single');
143 var btn = slide.querySelector('.slider-cart-network-button'); 144 if (card) { 145 card.style.transition = 'background-color 500ms ease'; 146 card.style.backgroundColor = active ? 'rgba(1, 17, 64, 0.85)' : 'rgba(255, 127, 17, 0.2)'; 147 } 148 if (btn) { 149 btn.style.transition = 'opacity 500ms ease'; 150 btn.style.opacity = active ? '1' : '0'; 151 } 152 } 153 154 function goTo(idx) { 155 if (infinite) { 156 current = ((idx % total) + total) % total; 157 } else { 158 current = Math.max(0, Math.min(idx, total - 1)); 159 } 160 mask.style.transition = 'transform 500ms ease'; 161 mask.style.transform = 'translateX(' + (-slides[current].offsetLeft) + 'px)'; 162 slides.forEach(function (s, i) { styleSlide(s, i === current); }); 163 updateDots(); 164 } 165 166 function updateDots() { 167 if (!nav) return; 168 nav.querySelectorAll('.w-slider-dot').forEach(function (d, i) { 169 d.classList.toggle('w-active', i === current); 170 }); 171 } 172 173 styleSlide(slides[0], true); 174 for (var s = 1; s < total; s++) styleSlide(slides[s], false); 175 176 if (nav) { 177 var isNum = nav.classList.contains('w-num'); 178 for (var i = 0; i < total; i++) { 179 var dot = document.createElement('div'); 180 dot.className = 'w-slider-dot' + (i === 0 ? ' w-active' : ''); 181 if (isNum) dot.textContent = i + 1; 182 dot.setAttribute('data-index', i); 183 dot.addEventListener('click', function () { 184 goTo(parseInt(this.getAttribute('data-index'))); 185 restartAuto(); 186 }); 187 nav.appendChild(dot); 188 } 189 } 190 191 if (leftArrow) leftArrow.addEventListener('click', function () { goTo(current - 1); restartAuto(); }); 192 if (rightArrow) rightArrow.addEventListener('click', function () { goTo(current + 1); restartAuto(); }); 193 194 var touchStartX = 0; 195 var touchDelta = 0; 196 mask.addEventListener('touchstart', function (e) { 197 touchStartX = e.touches[0].clientX; 198 touchDelta = 0; 199 mask.style.transition = 'none'; 200 }, { passive: true }); 201 mask.addEventListener('touchmove', function (e) { 202 touchDelta = e.touches[0].clientX - touchStartX; 203 mask.style.transform = 'translateX(' + (-slides[current].offsetLeft + touchDelta) + 'px)'; 204 }, { passive: true }); 205 mask.addEventListener('touchend', function () { 206 if (Math.abs(touchDelta) > 50) { 207 goTo(current + (touchDelta < 0 ? 1 : -1)); 208 } else { 209 goTo(current); 210 } 211 restartAuto(); 212 }); 213 214 function startAuto() { 215 if (!autoplay || autoDelay <= 0) return; 216 autoTimer = setInterval(function () { 217 var menuBtn = document.querySelector('.w-nav-button'); 218 if (menuBtn && menuBtn.classList.contains('w--open')) return; 219 goTo(current + 1); 220 }, autoDelay); 221 } 222 223 function restartAuto() { 224 clearInterval(autoTimer); 225 startAuto(); 226 } 227 228 window.addEventListener('resize', function () { goTo(current); }); 229 230 slider.querySelectorAll('.slider-single-cart').forEach(function (card) { 231 card.style.cursor = 'pointer'; 232 card.style.position = 'relative'; 233 card.style.zIndex = '20'; 234 }); 235 document.addEventListener('click', function (e) { 236 if (e.target.closest('.slider-single-cart')) { 237 e.preventDefault(); 238 e.stopPropagation(); 239 var dl = document.getElementById('download'); 240 if (dl) dl.scrollIntoView({ behavior: 'smooth' }); 241 } 242 }, true); 243 244 startAuto(); 245 }); 246 } 247 248 // ââ Scroll Animations (replaces IX2) âââââââââââââââââââââââââââââââ 249 function initScrollAnimations() { 250 var targets = document.querySelectorAll('[data-w-id]'); 251 var animatable = []; 252 253 targets.forEach(function (el) { 254 var op = el.style.opacity; 255 var tr = el.style.transform; 256 if (op === '0' || (tr && tr.indexOf('translate3d') !== -1 && tr.indexOf('rotateX') !== -1)) { 257 animatable.push(el); 258 } 259 }); 260 261 if (!animatable.length) return; 262 263 if (!('IntersectionObserver' in window)) { 264 animatable.forEach(function (el) { 265 el.style.opacity = '1'; 266 el.style.transform = 'none'; 267 }); 268 return; 269 } 270 271 var observer = new IntersectionObserver(function (entries) {
272 entries.forEach(function (entry) { 273 if (!entry.isIntersecting) return; 274 var el = entry.target; 275 el.style.transition = 'opacity 0.8s ease, transform 0.8s ease'; 276 el.style.opacity = '1'; 277 if (el.style.transform && el.style.transform.indexOf('rotateX') !== -1) { 278 el.style.transform = 'translate3d(0, 0, 0) scale3d(1, 1, 1) rotateX(0) rotateY(0) rotateZ(0) skew(0, 0)'; 279 } 280 observer.unobserve(el); 281 }); 282 }, { threshold: 0.15, rootMargin: '0px 0px -50px 0px' }); 283 284 animatable.forEach(function (el) { observer.observe(el); }); 285 } 286 287 // Shared by hash-anchor clicks, referral landing, and the APK bonus banner. 288 // Rechecks after layout/images settle because content above #download 289 // (lazy images, testimonials) often shifts the target. A new call cancels 290 // the previous correction loop so two scrolls cannot fight. 291 var cancelScrollToElement = function () {}; 292 293 function scrollToElement(el, opts) { 294 opts = opts || {}; 295 var behavior = opts.instant ? 'auto' : 'smooth'; 296 var correctAfter = opts.instant ? 0 : 600; 297 cancelScrollToElement(); 298 299 function jump(mode) { 300 var offset = opts.offset || 0; 301 var top = el.getBoundingClientRect().top + window.pageYOffset - offset; 302 window.scrollTo({ top: Math.max(0, top), behavior: mode || behavior }); 303 } 304 jump(); 305 306 var startTime = Date.now(); 307 var lastCheck = 0; 308 var cancelled = false; 309 var offset = opts.offset || 0; 310 311 function cancel() { 312 cancelled = true; 313 window.removeEventListener('wheel', cancel); 314 window.removeEventListener('touchmove', cancel); 315 window.removeEventListener('resize', onResize); 316 } 317 function onResize() { 318 if (!cancelled) jump('auto'); 319 } 320 cancelScrollToElement = cancel; 321 window.addEventListener('wheel', cancel, { passive: true, once: true }); 322 window.addEventListener('touchmove', cancel, { passive: true, once: true }); 323 window.addEventListener('resize', onResize); 324 325 function correct() { 326 if (cancelled) return; 327 var elapsed = Date.now() - startTime; 328 if (elapsed > 4000) { cancel(); return; } 329 if (elapsed > correctAfter && elapsed - lastCheck > 200) { 330 lastCheck = elapsed; 331 var off = el.getBoundingClientRect().top - offset; 332 if (Math.abs(off) > 20) { 333 window.scrollTo({ top: window.pageYOffset + off, behavior: 'auto' }); 334 } 335 } 336 requestAnimationFrame(correct); 337 } 338 requestAnimationFrame(correct); 339 } 340 341 // ââ Anchor Scroll ââââââââââââââââââââââââââââââââââââââââââââââââââ 342 function initAnchorScroll() { 343 344 function isSamePage(href) { 345 var a = document.createElement('a'); 346 a.href = href; 347 return a.pathname.replace(/\/$/, '') === window.location.pathname.replace(/\/$/, ''); 348 } 349 350 document.addEventListener('click', function (e) { 351 var link = e.target.closest('a[href*="#"]'); 352 if (!link) return; 353 var href = link.getAttribute('href'); 354 var hashIdx = href.indexOf('#'); 355 if (hashIdx === -1) return; 356 var hash = href.substring(hashIdx); 357 if (!hash || hash === '#') return; 358 var path = href.substring(0, hashIdx); 359 if (path && !isSamePage(href)) return; 360 var target = document.querySelector(hash); 361 if (!target) return; 362 e.preventDefault(); 363 history.pushState(null, null, hash); 364 scrollToElement(target); 365 }); 366 367 if (window.location.hash) { 368 window.addEventListener('load', function () { 369 var target = document.querySelector(window.location.hash); 370 if (target) { 371 setTimeout(function () { scrollToElement(target); }, 100); 372 } 373 }); 374 } 375 } 376 377 // ââ Testimonial horizontal scroll ââââââââââââââââââââââââââââââââââ 378 function initTestimonialScroll() { 379 var section = document.querySelector('.section.testimonial'); 380 var grid = section && section.querySelector('.testimonial-grid'); 381 if (!section || !grid) return; 382
383 function update() { 384 var vw = window.innerWidth; 385 // On tablet/mobile the CSS switches the grid to a normal block layout, 386 // so we only need the effect on wider screens. 387 if (vw <= 991) { 388 grid.style.transform = ''; 389 return; 390 } 391 392 var rect = section.getBoundingClientRect(); 393 var sectionH = section.offsetHeight; // 200vh 394 var viewH = window.innerHeight; 395 396 // progress: 0 when top of section hits viewport bottom â 1 when bottom of section hits viewport top 397 // We clamp to 0â1 so nothing happens outside the section. 398 var scrolled = -rect.top; // how many px the section has scrolled upward 399 var totalTravel = sectionH - viewH; // total scroll distance while section is "active" 400 var progress = Math.min(1, Math.max(0, scrolled / totalTravel)); 401 402 // How far we need to translate: grid width minus the visible container width 403 var containerW = section.querySelector('.testimonial-content-wrapper') 404 ? section.querySelector('.testimonial-content-wrapper').offsetWidth 405 : vw - 80; // fallback: viewport minus rough gutters 406 var maxShift = grid.scrollWidth - containerW; 407 if (maxShift <= 0) return; 408 409 grid.style.transform = 'translateX(' + (-progress * maxShift) + 'px)'; 410 grid.style.willChange = 'transform'; 411 } 412 413 window.addEventListener('scroll', update, { passive: true }); 414 window.addEventListener('resize', update, { passive: true }); 415 update(); 416 } 417 418 // ââ Packages / Pricing Loader ââââââââââââââââââââââââââââââââââââââ 419 function initPackagesLoader() { 420 if (!document.querySelector('.grid-pricing-plan')) return; 421 422 var path = window.location.pathname; 423 var lang = 'en'; 424 if (path.startsWith('/vpn/ru/') || path === '/ru') lang = 'ru'; 425 else if (path.startsWith('/vpn/zh/') || path === '/zh') lang = 'zh'; 426 else if (path.startsWith('/vpn/fa/') || path === '/fa') lang = 'fa'; 427 428 function formatPrice(pkg) { 429 if (lang === 'ru') return pkg.price_rub + ' RUB'; 430 if (lang === 'zh') return '\u00a5' + pkg.price_cny; 431 return '$' + pkg.price_usd.toFixed(2); 432 } 433 434 fetch('../packages.json') 435 .then(function (r) { return r.json(); }) 436 .then(function (data) { 437 var packages = data.packages; 438 var planItems = document.querySelectorAll('.grid-pricing-plan .inner-tab-link'); 439 planItems.forEach(function (item, i) { 440 var pkg = packages[i]; 441 if (!pkg) return; 442 var priceEl = item.querySelector('.price-2'); 443 if (priceEl) priceEl.textContent = formatPrice(pkg); 444 }); 445 }) 446 .catch(function () {}); 447 } 448 449 // ââ Referral handling + GA event tracking ââââââââââââââââââââââââââ 450 // Ports the legacy referral flow into the new design and adds GA4 events 451 // for every download/CTA button. Safe to run on any page â it no-ops if 452 // the relevant DOM nodes don't exist. 453 function initReferralAndTracking() { 454 var langCfg = window.GFVPN_LANG || {}; 455 var messages = langCfg.messages || {}; 456 var currentLang = langCfg.lang || (function () { 457 var m = window.location.pathname.match(/^\/(en|ru|zh|fa|vi|my|tr|es)/); 458 return m ? m[1] : 'en'; 459 })(); 460 461 var urlParams = new URLSearchParams(window.location.search); 462 var referralCode = urlParams.get('code'); 463 var userAgent = navigator.userAgent || navigator.vendor || window.opera || ''; 464 465 function track(eventName, params) { 466 if (typeof window.gtag === 'function') { 467 window.gtag('event', eventName, params || {}); 468 } 469 } 470 471 var CONFIG = { 472 PLAY_STORE_URL: 'https://play.google.com/store/apps/details?id=org.greatfire.gfvpn', 473 APP_STORE_URL: 'https://apps.apple.com/app/greatfirevpn/id6761056824' 474 }; 475 476 var downloadSection = document.getElementById('download'); 477 // Prefer stable IDs; fall back to href-substring selectors so the script 478 // keeps working even if the IDs are accidentally removed. 479 var googlePlayLink = document.getElementById('google-play-link') || 480 (downloadSection && downloadSection.querySelector('a[href*="play.google.com"]')); 481 var appStoreLink = document.getElementById('app-store-button') || 482 (downloadSection && downloadSection.querySelector('a[href*="apps.apple.com"]'));
483 var apkLink = document.getElementById('apk-download-link') || 484 (downloadSection && downloadSection.querySelector('a[href*=".apk"]')); 485 var windowsLink = document.getElementById('windows-download-link') || 486 (downloadSection && downloadSection.querySelector('a[href*="GreatFireVPN-Setup.exe"]')); 487 488 var bonusLink = document.getElementById('activate-bonus-link'); 489 var copyIosLink = document.getElementById('copy-referral-ios'); 490 var copyWindowsLink = document.getElementById('copy-referral-windows'); 491 492 var downloadLinks = [googlePlayLink, appStoreLink, apkLink, windowsLink]; 493 494 // Propagate URL query (e.g. ?code=â¦) onto internal nav links so the 495 // referral code survives navigation to /payment, footer links, etc. 496 var search = window.location.search; 497 if (search) { 498 var searchBody = search.substring(1); 499 document.querySelectorAll('a[href]').forEach(function (link) { 500 var href = link.getAttribute('href'); 501 if (!href) return; 502 if (/^(https?:|javascript:|mailto:|tel:|#)/i.test(href)) return; 503 if (link.id === 'activate-bonus-link') return; 504 if (downloadLinks.indexOf(link) !== -1) return; 505 if (!href.startsWith('/vpn/')) return; 506 var hashIdx = href.indexOf('#'); 507 if (hashIdx !== -1) { 508 var beforeHash = href.substring(0, hashIdx); 509 var hash = href.substring(hashIdx); 510 var sep1 = beforeHash.indexOf('?') !== -1 ? '&' : '?'; 511 link.setAttribute('href', beforeHash + sep1 + searchBody + hash); 512 } else { 513 var sep2 = href.indexOf('?') !== -1 ? '&' : '?'; 514 link.setAttribute('href', href + sep2 + searchBody); 515 } 516 }); 517 } 518 519 // Referral landing: mark the download section so the 1â2 stepper CSS 520 // reveals Activate Bonus / Copy code, then scroll and track. 521 if (referralCode) { 522 if (downloadSection) { 523 downloadSection.classList.add('is-referral'); 524 var scrollToDownload = function () { 525 setTimeout(function () { scrollToElement(downloadSection); }, 100); 526 }; 527 if (document.readyState === 'complete') scrollToDownload(); 528 else window.addEventListener('load', scrollToDownload); 529 } 530 track('referral_landing', { 531 event_category: 'engagement', 532 referral_code: referralCode, 533 language: currentLang 534 }); 535 } 536 537 // Google Play: append &referrer=code%3D⦠so Play's Install Referrer API 538 // can hand the code to the app on first launch (auto-redemption). 539 if (googlePlayLink) { 540 if (referralCode) { 541 var base = googlePlayLink.getAttribute('href') || CONFIG.PLAY_STORE_URL; 542 var sep = base.indexOf('?') !== -1 ? '&' : '?'; 543 googlePlayLink.setAttribute('href', base + sep + 'referrer=code%3D' + encodeURIComponent(referralCode)); 544 } 545 googlePlayLink.addEventListener('click', function () { 546 track('app_download_android', { 547 event_category: 'download', 548 event_label: 'google_play_store', 549 download_type: 'google_play', 550 referral_code: referralCode || 'none' 551 }); 552 }); 553 } 554 555 // App Store: show an availability notice first (the app is region-removed 556 // in many of our users' countries). On "continue" we open the store and 557 // copy the referral code to the clipboard (the App Store strips referrers, 558 // so iOS users paste it into the app after installing). 559 if (appStoreLink) { 560 var appStoreGuideUrl = '/vpn/' + currentLang + '/install-deleted-appstore-app'; 561 var isRtlLang = currentLang === 'fa'; 562 var appStoreModal = null; 563 564 var onAppStoreModalKey = function (e) { 565 if (e.key === 'Escape') closeAppStoreModal(); 566 }; 567 568 var closeAppStoreModal = function () { 569 if (appStoreModal) appStoreModal.style.display = 'none'; 570 document.removeEventListener('keydown', onAppStoreModalKey); 571 }; 572 573 var proceedToAppStore = function () { 574 // Open inside the click gesture so popup blockers don't intervene. 575 window.open(CONFIG.APP_STORE_URL, '_blank', 'noopener'); 576 if (referralCode && navigator.clipboard && messages.clipboardSuccess) { 577 navigator.clipboard.writeText(referralCode).then(function () { 578 try { alert(messages.clipboardSuccess.replace('{code}', referralCode)); } catch (_) {} 579 }).catch(function () {}); 580 } 581 }; 582 583 var buildAppStoreModal = function () { 584 if (appStoreModal) return appStoreModal; 585 var title = messages.appStoreNoticeTitle || "Can't find the app? Read this first"; 586 var body = messages.appStoreNoticeBody || "GreatFireVPN may not appear in the App Store in y
586our region. If you can't find it, you'll most likely need a free second Apple ID â about 10 minutes, no card needed."; 587 var contLabel = messages.appStoreNoticeContinue || 'Continue to App Store'; 588 var guideLabel = messages.appStoreNoticeGuide || 'How to install'; 589 590 var overlay = document.createElement('div'); 591 overlay.setAttribute('role', 'dialog'); 592 overlay.setAttribute('aria-modal', 'true'); 593 overlay.style.cssText = 'position:fixed;inset:0;z-index:10000;display:none;align-items:center;justify-content:center;background:rgba(1,17,64,.55);padding:20px;'; 594 595 var card = document.createElement('div'); 596 card.setAttribute('dir', isRtlLang ? 'rtl' : 'ltr'); 597 card.style.cssText = 'background:#fff;border-radius:20px;max-width:430px;width:100%;padding:30px 26px;box-shadow:0 24px 60px rgba(1,17,64,.4);text-align:' + (isRtlLang ? 'right' : 'left') + ';'; 598 599 card.innerHTML = 600 '<div style="display:flex;gap:12px;align-items:flex-start;margin-bottom:14px;">' + 601 '<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#e25b00" stroke-width="2" style="flex:0 0 auto;margin-top:1px;"><path d="M12 9v4m0 4h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/></svg>' + 602 '<h3 style="margin:0;font-family:\'IBM Plex Sans\',sans-serif;font-weight:600;font-size:20px;line-height:1.25;color:#011140;">' + title + '</h3>' + 603 '</div>' + 604 '<p style="margin:0 0 22px;font-family:\'Open Sans\',sans-serif;font-size:15px;line-height:1.55;color:#4a4a4a;">' + body + '</p>' + 605 '<div style="display:flex;flex-direction:column;gap:10px;">' + 606 '<a href="' + appStoreGuideUrl + '" data-ag-guide style="display:block;text-align:center;text-decoration:none;border:1px solid #eff0f6;border-radius:12px;padding:12px 20px;font-family:\'Open Sans\',sans-serif;font-size:15px;font-weight:600;color:#011140;background:#fff;">' + guideLabel + '</a>' + 607 '<button type="button" data-ag-continue style="cursor:pointer;border:0;border-radius:12px;padding:13px 20px;font-family:\'IBM Plex Sans\',sans-serif;font-size:16px;font-weight:600;color:#fff;background-image:linear-gradient(90deg,#e70e02,#ff7f11);">' + contLabel + '</button>' + 608 '</div>'; 609 610 overlay.appendChild(card); 611 overlay.addEventListener('click', function (e) { 612 if (e.target === overlay) closeAppStoreModal(); 613 }); 614 card.querySelector('[data-ag-continue]').addEventListener('click', function () { 615 track('appstore_notice_continue', { event_category: 'engagement', language: currentLang }); 616 closeAppStoreModal(); 617 proceedToAppStore(); 618 }); 619 card.querySelector('[data-ag-guide]').addEventListener('click', function () { 620 track('appstore_notice_guide', { event_category: 'engagement', language: currentLang }); 621 }); 622 623 document.body.appendChild(overlay); 624 appStoreModal = overlay; 625 return overlay; 626 }; 627 628 appStoreLink.addEventListener('click', function (e) { 629 e.preventDefault(); 630 track('app_download_ios', { 631 event_category: 'download', 632 event_label: 'app_store', 633 download_type: 'app_store', 634 referral_code: referralCode || 'none' 635 }); 636 var modal = buildAppStoreModal(); 637 modal.style.display = 'flex'; 638 document.addEventListener('keydown', onAppStoreModalKey); 639 }); 640 } 641 642 // Trigger a same-origin file download via a synthetic <a download> click. 643 // This avoids the "stray about:blank tab" issue caused by the link's 644 // target="_blank" attribute (Chromium opens the new tab synchronously 645 // before our click handler can preventDefault, especially after a 646 // confirm() dialog interrupts the click). 647 function triggerDownload(href, suggestedName) { 648 var dl = document.createElement('a'); 649 dl.href = href; 650 dl.setAttribute('download', suggestedName || ''); 651 dl.style.display = 'none'; 652 document.body.appendChild(dl); 653 dl.click(); 654 document.body.removeChild(dl); 655 } 656 657 // APK: confirm popup explaining the bonus flow when there's a referral
658 // code, then track and trigger the download. Activate Bonus stays visible 659 // under the APK button so the user can tap it after installing. 660 if (apkLink) { 661 apkLink.addEventListener('click', function (e) { 662 e.preventDefault(); 663 if (referralCode && messages.apkDownloadPopup) { 664 var ok = confirm(messages.apkDownloadPopup.replace('{code}', referralCode)); 665 if (!ok) return; 666 } 667 track('app_download_apk', { 668 event_category: 'download', 669 event_label: 'apk_download', 670 download_type: 'apk', 671 referral_code: referralCode || 'none' 672 }); 673 triggerDownload(apkLink.getAttribute('href'), 'greatfirevpn.apk'); 674 }); 675 } 676 677 // Windows: same paste-in-Settings flow as iOS. Copy the code, explain 678 // in a confirm dialog, then start the installer download. 679 if (windowsLink) { 680 windowsLink.addEventListener('click', function (e) { 681 e.preventDefault(); 682 if (referralCode && messages.windowsDownloadPopup) { 683 if (navigator.clipboard && navigator.clipboard.writeText) { 684 navigator.clipboard.writeText(referralCode).catch(function () {}); 685 } 686 var ok = confirm(messages.windowsDownloadPopup.replace('{code}', referralCode)); 687 if (!ok) return; 688 } 689 track('app_download_windows', { 690 event_category: 'download', 691 event_label: 'windows_installer', 692 download_type: 'windows', 693 referral_code: referralCode || 'none' 694 }); 695 triggerDownload(windowsLink.getAttribute('href'), 'GreatFireVPN-Setup.exe'); 696 }); 697 } 698 699 function copyReferralCode(source) { 700 if (!referralCode) return; 701 track('referral_code_copy', { 702 event_category: 'engagement', 703 referral_code: referralCode, 704 copy_source: source 705 }); 706 var msg = (messages.clipboardSuccess || 'Referral code {code} copied! Paste it in the app after installing.').replace('{code}', referralCode); 707 var show = function () { 708 try { alert(msg); } catch (_) {} 709 }; 710 if (navigator.clipboard && navigator.clipboard.writeText) { 711 navigator.clipboard.writeText(referralCode).then(show).catch(show); 712 } else { 713 show(); 714 } 715 } 716 717 if (copyIosLink) { 718 copyIosLink.addEventListener('click', function (e) { 719 e.preventDefault(); 720 copyReferralCode('ios'); 721 }); 722 } 723 if (copyWindowsLink) { 724 copyWindowsLink.addEventListener('click', function (e) { 725 e.preventDefault(); 726 copyReferralCode('windows'); 727 }); 728 } 729 730 // Activate Bonus: try to open the installed app, then fall back to the 731 // greatfirevpn.com App Link if the page is still visible after 2.5s 732 // (in-app browsers that ignore intent:// / gfvpn://). 733 // Chrome: intent:// unwraps to gfvpn:// for package org.greatfire.gfvpn. 734 // Firefox: raw gfvpn:// (it does not unwrap intent://). 735 if (bonusLink) { 736 bonusLink.addEventListener('click', function (e) { 737 e.preventDefault(); 738 e.stopPropagation(); 739 track('referral_bonus_click', { 740 event_category: 'conversion', 741 referral_code: referralCode || 'unknown' 742 }); 743 if (!referralCode) return; 744 var encodedCode = encodeURIComponent(referralCode); 745 var isAndroid = /android/i.test(userAgent); 746 if (isAndroid) { 747 var isFirefox = /firefox/i.test(userAgent); 748 var openLink = isFirefox 749 ? 'gfvpn://referral?code=' + encodedCode 750 : 'intent://referral?code=' + encodedCode + 751 '#Intent;scheme=gfvpn;package=org.greatfire.gfvpn;end'; 752 var fallbackLink = '/vpn/referral?code=' + encodedCode; 753 var appOpened = false; 754 var onVis = function () { if (document.hidden) appOpened = true; }; 755 var onBlur = function () { appOpened = true; }; 756 document.addEventListener('visibilitychange', onVis); 757 window.addEventListener('blur', onBlur); 758 window.location.href = openLink; 759 setTimeout(function () { 760 document.removeEventListener('visibilitychange', onVis); 761 window.removeEventListener('blur', onBlur); 762 if (!appOpened && !document.hidden) { 763 window.location.href = fallbackLink; 764 } 765 }, 2500); 766 } else { 767 alert(messages.androidOnly || 'This feature is only available on Android. Please download the APK and activate your bonus from an Android device.'); 768 } 769 }); 770 } 771 772 // Top-up CTA tracking (nav + footer "Top up" links). 773 document.querySelectorAll('a[href*="/payment"]').forEach(function (link) { 774 link.addEventListener('click', function () { 775 track('top_up_click', { 776 event_category: 'conversion', 777 event_label: 'top_up_balance' 778 }); 779 }); 780 }); 781 782 // Language change tracking (nav dropdown). 783 document.querySelectorAll('.language-dropdown-wrapper a.dropdown-link').forEach(function (link) { 784 link.addEventListener('click', function () { 785 var href = link.getAttribute('href') || ''; 786 var m = href.match(/^\/(en|ru|zh|fa|vi|my|tr|es)\b/); 787 track('language_change', { 788 event_category: 'engagement', 789 language_from: currentLang, 790 language_to: m ? m[1] : 'unknown' 791 }); 792 }); 793 }); 794 795 // App Store install help link tracking. 796 document.querySelectorAll('a[href*="install-deleted-appstore-app"]').forEach(function (link) { 797 link.addEventListener('click', function () { 798 track('appstore_help_click', { 799 event_category: 'engagement', 800 event_label: 'app_store_install_help' 801 }); 802 }); 803 }); 804 } 805 806 // Cloudflare Turnstile site key from the Brevo form (verified by Brevo). 807 var TURNSTILE_SITE_KEY = '0x4AAAAAAEyFDGhpq9bmQMJE'; 808 809 window.handleCaptchaResponse = function () { 810 window.grecaptcha = window.turnstile; 811 }; 812 813 function loadTurnstile(done) { 814 if (window.turnstile) { 815 done(null); 816 return; 817 } 818 var settled = false;
819 var finish = function (err) { 820 if (settled) return; 821 settled = true; 822 done(err || null); 823 }; 824 var timer = setTimeout(function () { finish(new Error('timeout')); }, 8000); 825 var s = document.createElement('script'); 826 s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; 827 s.async = true; 828 s.onload = function () { 829 clearTimeout(timer); 830 finish(window.turnstile ? null : new Error('missing')); 831 }; 832 s.onerror = function () { 833 clearTimeout(timer); 834 finish(new Error('error')); 835 }; 836 document.head.appendChild(s); 837 } 838 839 var NL_STORE_KEY = 'gfvpn_nl_subscribed'; 840 var NL_STORE_MAX_AGE = 365 * 24 * 60 * 60; 841 842 function hasNewsletterSubscribed() { 843 try { 844 if (window.localStorage && localStorage.getItem(NL_STORE_KEY) === '1') return true; 845 } catch (e) { /* ignore */ } 846 try { 847 var parts = document.cookie.split(';'); 848 for (var i = 0; i < parts.length; i++) { 849 if (parts[i].trim() === NL_STORE_KEY + '=1') return true; 850 } 851 } catch (e) { /* ignore */ } 852 return false; 853 } 854 855 function markNewsletterSubscribed() { 856 try { localStorage.setItem(NL_STORE_KEY, '1'); } catch (e) { /* ignore */ } 857 try { 858 var secure = window.location.protocol === 'https:' ? '; Secure' : ''; 859 document.cookie = NL_STORE_KEY + '=1; Path=/; Max-Age=' + NL_STORE_MAX_AGE + '; SameSite=Lax' + secure; 860 } catch (e) { /* ignore */ } 861 } 862 863 function showNewsletterThanks(form, successMsg, scroll) { 864 if (successMsg) successMsg.style.display = 'block'; 865 if (form) form.style.display = 'none'; 866 if (scroll && successMsg && successMsg.scrollIntoView) { 867 successMsg.scrollIntoView({ behavior: 'smooth', block: 'center' }); 868 } 869 } 870 871 function initNewsletterForm() { 872 var form = document.getElementById('sib-form'); 873 if (!form) return; 874 875 var email = form.querySelector('#EMAIL'); 876 var honeypot = form.querySelector('input[name="email_address_check"]'); 877 var btn = form.querySelector('.footer-subscribe-btn'); 878 var btnText = form.querySelector('.footer-subscribe-btn-text'); 879 var errLabel = form.querySelector('.footer-subscribe-error'); 880 var successMsg = document.getElementById('success-message'); 881 var errorMsg = document.getElementById('error-message'); 882 var captchaHost = form.querySelector('.footer-subscribe-captcha'); 883 var loadingText = form.getAttribute('data-msg-loading') || 'Subscribingâ¦'; 884 var invalidText = form.getAttribute('data-msg-invalid') || 'Please enter a valid email address.'; 885 var captchaText = form.getAttribute('data-msg-captcha') || 'Please complete the verification.'; 886 var defaultText = btnText ? btnText.textContent : ''; 887 var emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 888 var widgetId = null; 889 var captchaActive = false; 890 var captchaRequired = !!TURNSTILE_SITE_KEY; 891 892 if (hasNewsletterSubscribed()) { 893 showNewsletterThanks(form, successMsg, false); 894 return; 895 } 896 897 function resetMsgs() { 898 if (successMsg) successMsg.style.display = 'none'; 899 if (errorMsg) errorMsg.style.display = 'none'; 900 if (errLabel) errLabel.textContent = ''; 901 } 902 903 function loading(on) { 904 if (!btn) return; 905 btn.disabled = on; 906 if (on) { 907 btn.classList.add('is-loading'); 908 if (btnText) btnText.textContent = loadingText; 909 } else { 910 btn.classList.remove('is-loading'); 911 if (btnText) btnText.textContent = defaultText; 912 } 913 } 914 915 function resetCaptcha() { 916 if (captchaActive && window.turnstile && widgetId !== null) { 917 try { window.turnstile.reset(widgetId); } catch (e) { /* ignore */ } 918 } 919 } 920 921 if (captchaRequired && captchaHost) { 922 loadTurnstile(function (err) { 923 if (err || !window.turnstile) return; 924 widgetId = window.turnstile.render(captchaHost, { 925 sitekey: TURNSTILE_SITE_KEY, 926 theme: 'light', 927 size: 'flexible', 928 appearance: 'always', 929 language: document.documentElement.lang || 'auto', 930 callback: function () { 931 if (typeof window.handleCaptchaResponse === 'function') { 932 window.handleCaptchaResponse(); 933 } 934 } 935 }); 936 captchaActive = true; 937 }); 938 } 939 940 form.addEventListener('submit', function (e) { 941 e.preventDefault(); 942 resetMsgs(); 943 if (honeypot && honeypot.value) return; 944 var value = email ? email.value.trim() : ''; 945 if (!emailRe.test(value)) { 946 if (errLabel) errLabel.textContent = invalidText; 947 if (email) email.focus(); 948 return; 949 } 950 if (captchaRequired) { 951 var token = ''; 952 if (captchaActive && window.turnstile) { 953 try { token = window.turnstile.getResponse(widgetId) || ''; } catch (err) { token = ''; } 954 } 955 if (!token) { 956 var hidden = form.querySelector('[name="cf-turnstile-response"]'); 957 token = hidden && hidden.value ? hidden.value : ''; 958 } 959 if (!token) { 960 if (errLabel) errLabel.textContent = captchaText; 961 return; 962 } 963 } 964 loading(true); 965 var body = new URLSearchParams(new FormData(form)).toString(); 966 fetch(form.action, { 967 method: 'POST', 968 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 969 body: body 970 }).then(function (r) { 971 return r.json().catch(function () { return { success: false }; }); 972 }).then(function (res) { 973 if (res && res.success) { 974 markNewsletterSubscribed(); 975 showNewsletterThanks(form, successMsg, true); 976 } else { 977 if (errorMsg) errorMsg.style.display = 'block'; 978 loading(false); 979 resetCaptcha(); 980 } 981 }).catch(function () { 982 if (errorMsg) errorMsg.style.display = 'block'; 983 loading(false); 984 resetCaptcha(); 985 }); 986 }); 987 } 988 989 // ââ Init ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 990 if (document.readyState === 'loading') { 991 document.addEventListener('DOMContentLoaded', init); 992 } else { 993 init(); 994 } 995
996 function init() { 997 initHamburger(); 998 initNavbar(); 999 initDropdowns(); 1000 initSliders(); 1001 initScrollAnimations(); 1002 initAnchorScroll(); 1003 initTestimonialScroll(); 1004 initPackagesLoader(); 1005 initReferralAndTracking(); 1006 initNewsletterForm(); 1007 } 1008})();
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.