1/* eslint-env jquery */ 2function randomString(n = 12) { 3 const characters = 4 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 5 let result = "" 6 const charactersLength = characters.length 7 8 for (let i = 0; i < n; i++) { 9 result += characters.charAt( 10 Math.floor(Math.random() * charactersLength) 11 ) 12 } 13 14 return result 15} 16 17function loadScript(src) { 18 if (document.querySelector(`script[src="${src}"]`)) return 19 20 const tag = document.createElement("script") 21 tag.async = true 22 tag.src = src 23 tag.defer = true 24 const firstScriptTag = document.getElementsByTagName("script")[0] 25 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag) 26} 27 28const STRIPE_JS_URL = "https://js.stripe.com/v3/" 29let stripeJsPromise = null 30 31// The server only ships Stripe.js on pages it detects as taking payments. If 32// that detection ever misses, load the library on demand instead of throwing 33// on `window.Stripe` halfway through a checkout. 34function ensureStripe() { 35 if (window.Stripe) return Promise.resolve() 36 if (stripeJsPromise) return stripeJsPromise 37 38 stripeJsPromise = new Promise((resolve, reject) => { 39 const tag = document.createElement("script") 40 tag.src = STRIPE_JS_URL 41 tag.onload = () => resolve() 42 tag.onerror = () => { 43 stripeJsPromise = null 44 reject(new Error("Failed to load Stripe.js")) 45 } 46 document.head.appendChild(tag) 47 }) 48 return stripeJsPromise 49} 50 51function injectYoutubeScript() { 52 if (window.YT) return 53 loadScript("https://www.youtube.com/iframe_api") 54} 55 56function injectVimeoScript() { 57 if (window.Vimeo) return 58 loadScript("https://player.vimeo.com/api/player.js") 59} 60 61function injectWistiaScript() { 62 if (window._wq) return 63 loadScript("//fast.wistia.net/assets/external/E-v1.js") 64} 65 66function checkMEPlayerAvailable(callback) { 67 if (!window.MediaElementPlayer) { 68 setTimeout(() => { 69 checkMEPlayerAvailable(callback) 70 }, 50) 71 } else if (typeof callback === "function") { 72 callback() 73 } 74} 75 76const getYoutubeVideoId = (videoUrl) => { 77 if (!videoUrl) return null 78 const match = videoUrl.match( 79 /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/ 80 ) 81 if (!match || match.length < 1) { 82 return null // Invalid YouTube URL 83 } 84 85 return match[1] 86} 87 88function getVimeoVideoId(url) { 89 if (!url) return null 90 const regex = 91 /(?:https?:\/\/)?(?:www\.)?vimeo\.com\/(?:channels\/\w+\/|groups\/\w+\/videos\/|video\/|)(\d+)/ 92 const match = url.match(regex) 93 return match ? match[1] : null 94} 95 96const getWistiaVideoId = (videoUrl) => { 97 if (!videoUrl) return null 98 const match = videoUrl.match( 99 /(?:https?:\/\/)?(?:www\.)?(?:wistia\.com|wi\.st)\/(?:medias|embed)\/([a-zA-Z0-9]+)/ 100 ) 101 if (!match || match.length < 1) { 102 return null // Invalid Wistia URL 103 } 104 105 return match[1] 106} 107 108const initializeYouTubePlayer = async (playerId, videoId, options = {}) => { 109 // Inject YouTube API script if not already present 110 if (!window.YT) { 111 injectYoutubeScript() 112 } 113 114 // Wait for the YouTube API to be available 115 const waitForYouTube = async () => { 116 while (!window.YT || !window.YT.Player) { 117 await new Promise((resolve) => setTimeout(resolve, 50)) // Retry every 50ms 118 } 119 } 120 121 await waitForYouTube() // Ensure YouTube API is loaded 122 123 // Create and return the YouTube Player instance 124 return new window.YT.Player(playerId, { 125 videoId, 126 playerVars: { 127 playlist: videoId, 128 rel: 0, 129 showInfo: 0, 130 playsInline: 1, 131 mute: 1, 132 autoplay: 1, 133 loop: 1, 134 disablekb: 1, 135 controls: 0, 136 iv_load_policy: 1, 137 cc_load_policy: 0, 138 ...options.playerVars // Allow overriding playerVars 139 }, 140 events: options.events // Allow overriding events 141 }) 142} 143 144const initializeVimeoPlayer = async (playerId, videoId, options = {}) => { 145 if (!window.Vimeo) { 146 injectVimeoScript() 147 } 148 149 // Wait for the Vimeo API to be available 150 const waitForVimeo = async () => { 151 while (!window.Vimeo) { 152 await new Promise((resolve) => setTimeout(resolve, 50)) // Retry every 50ms 153 } 154 } 155 156 await waitForVimeo() // Ensure Vimeo API is loaded 157 158 // Create and return the Vimeo Player instance 159 const playerOptions = { 160 id: parseInt(videoId), 161 autoplay: true, 162 muted: true, 163 background: true, 164 playsinline: true, 165 loop: true, 166 byline: false, 167 title: false, 168 portrait: false, 169 controls: false, 170 ...options // Allow overriding default options 171 } 172 173 return new window.Vimeo.Player(playerId, playerOptions) 174} 175 176const initializeWistiaPlayer = async (playerId, videoId, options = {}) => { 177 if (!window._wq) { 178 injectWistiaScript() 179 } 180 181 // Wait for the Wistia API to be available 182 const waitForWistia = async () => { 183 while (!window._wq) { 184 await new Promise((resolve) => setTimeout(resolve, 50)) // Retry every 50ms 185 } 186 } 187 188 await waitForWistia() 189 190 // Create the Wistia iframe and attach it to the specified player container 191 const iframe = document.createElement("iframe") 192 iframe.width = "100%" 193 iframe.height = "100%" 194 iframe.src = `https://fast.wistia.net/embed/iframe/${videoId}?autoplay=1&controls=0&loop=1&wmode=transparent&muted=1` 195 iframe.name = "wistia_embed" 196 iframe.frameBorder = "0" 197 document.getElementById(playerId).appendChild(iframe) 198 199 // Initialize the Wistia player using _wq 200 window._wq = window._wq || [] 201 window._wq.push({ 202 id: videoId, 203 onReady: function (video) { 204 if (options.onReady) options.onReady(video) 205 } 206 }) 207} 208
209async function loadVideoBackground() { 210 const element = document.querySelector("[data-video-background]") 211 if (!element) return 212 let data = {} 213 try { 214 data = JSON.parse(element.dataset.videoBackground) 215 } catch (error) { 216 console.warn("invalid video background settings.") 217 return 218 } 219 element.removeAttribute("data-video-background") 220 if (!data?.video || data?.enableVideo !== "Yes") return 221 let videoId = null 222 223 if (data.videoType === "youtube") { 224 videoId = getYoutubeVideoId(data.video) 225 } else if (data.videoType === "vimeo") { 226 videoId = getVimeoVideoId(data.video) 227 } else if (data.videoType === "wistia") { 228 videoId = getWistiaVideoId(data.video) 229 } 230 231 if (!videoId) return 232 const playerId = `player_${randomString(10)}` 233 const containerId = `player_${randomString(10)}` 234 const ratio = 1.7777777778 // 16:9 235 const style = document.createElement("style") 236 style.innerHTML = ` 237 #${containerId} { 238 position: fixed; 239 top: 0; 240 left: 0; 241 bottom: 0; 242 right: 0; 243 width: 100%; 244 height: 100%; 245 pointer-events: none; 246 z-index: -1; 247 overflow: hidden; 248 } 249 #${playerId} { 250 top: 50%; 251 left: 50%; 252 transform: translateX(-50%) translateY(-50%); 253 position: absolute; 254 opacity: 0; 255 } 256 ` 257 const player = document.createElement("div") 258 player.id = playerId 259 element.id = containerId 260 element.appendChild(player) 261 element.appendChild(style) 262 263 if (data.videoType === "youtube") { 264 await initializeYouTubePlayer(playerId, videoId, { 265 events: { 266 onStateChange(evt) { 267 if (evt.data === 1) { 268 document.getElementById(playerId).style.opacity = 1 269 } else { 270 document.getElementById(playerId).style.opacity = 0 271 } 272 } 273 } 274 }) 275 } else if (data.videoType === "vimeo") { 276 const vimeoPlayer = await initializeVimeoPlayer(playerId, videoId) 277 278 vimeoPlayer.on("loaded", () => { 279 vimeoPlayer.play() 280 const playerElement = vimeoPlayer.element 281 playerElement.style.width = "100%" 282 playerElement.style.height = "100%" 283 284 document.getElementById(playerId).style.opacity = 1 285 }) 286 } else if (data.videoType === "wistia") { 287 await initializeWistiaPlayer(playerId, videoId, { 288 onReady: (video) => { 289 document.getElementById(playerId).style.opacity = 1 290 291 video.bind("end", function () { 292 video.play() 293 }) 294 } 295 }) 296 } 297 298 function resize() { 299 const height = window.innerHeight 300 const width = window.innerWidth 301 const cont = document.getElementById(playerId) 302 if (ratio > width / height) { 303 cont.style.width = `${height * ratio}px` 304 cont.style.height = `${height}px` 305 } else { 306 cont.style.width = `${width}px` 307 cont.style.height = `${width / ratio}px` 308 } 309 } 310 resize() 311 window.addEventListener("resize", resize) 312} 313 314function openPopup(node, data) { 315 const modalId = "elMediaPopupContainer" 316 document.getElementById(modalId)?.remove() 317 318 document.body.style.overflow = "hidden" 319 const modal = document.createElement("div") 320 const wrapper = document.createElement("div") 321 const backdrop = document.createElement("div") 322 const content = document.createElement("div") 323 const button = document.createElement("button") 324 325 const closePopup = () => { 326 const threeDSWrapper = modal.querySelector(".tds-wrapper") 327 if (threeDSWrapper) { 328 on3DSComplete(data) 329 } 330 331 modal.classList.remove("enter-popup") 332 modal.classList.add("exit-popup") 333 334 setTimeout(() => { 335 document.getElementById(modalId)?.remove() 336 document.body.style.overflow = "" 337 }, 300) 338 } 339 340 modal.id = modalId 341 modal.classList.add("enter-popup") 342 backdrop.classList.add("popup-backdrop") 343 backdrop.addEventListener("click", closePopup) 344 wrapper.classList.add("popup-content-wrapper") 345 content.classList.add("popup-content") 346 button.innerHTML = ` 347 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" width="14" height="14" fill="currentColor"> 348 <path d="M376.6 84.5c11.3-13.6 9.5-33.8-4.1-45.1s-33.8-9.5-45.1 4.1L192 206 56.6 43.5C45.3 29.9 25.1 28.1 11.5 39.4S-3.9 70.9 7.4 84.5L150.3 256 7.4 427.5c-11.3 13.6-9.5 33.8 4.1 45.1s33.8 9.5 45.1-4.1L192 306 327.4 468.5c11.3 13.6 31.5 15.4 45.1 4.1s15.4-31.5 4.1-45.1L233.7 256 376.6 84.5z"/> 349 </svg>`
350 button.classList.add("gjs-popup-close") 351 button.addEventListener("click", closePopup) 352 353 content.appendChild(node) 354 wrapper.appendChild(button) 355 wrapper.appendChild(content) 356 modal.appendChild(backdrop) 357 modal.appendChild(wrapper) 358 359 document.body.appendChild(modal) 360} 361 362function openImagePopup(url) { 363 const img = document.createElement("img") 364 img.src = url 365 openPopup(img) 366} 367 368function openVideoPopup(src, conf) { 369 const div = document.createElement("div") 370 const iframe = document.createElement("iframe") 371 div.classList.add("popup-iframe-scaler") 372 373 iframe.style.border = "none" 374 iframe.frameborder = "0" 375 iframe.title = conf?.videoTitle 376 iframe.allowFullscreen = true 377 iframe.classList.add("popup-iframe") 378 iframe.allow = "autoplay" 379 iframe.referrerPolicy = "strict-origin-when-cross-origin" 380 iframe.src = src 381 div.appendChild(iframe) 382 383 openPopup(div) 384} 385 386function open3DSPopup(src, data) { 387 const div = document.createElement("div") 388 const iframe = document.createElement("iframe") 389 div.classList.add("popup-iframe-scaler", "tds-wrapper") 390 391 iframe.style.border = "none" 392 iframe.frameborder = "0" 393 iframe.classList.add("popup-iframe") 394 iframe.width = 600 395 iframe.height = 400 396 iframe.referrerPolicy = "strict-origin-when-cross-origin" 397 iframe.src = src 398 div.appendChild(iframe) 399 400 openPopup(div, data) 401} 402 403async function on3DSComplete(data) { 404 try { 405 const iFrame = document.querySelector(".tds-wrapper") 406 iFrame.remove() 407 408 const url = new URL( 409 `https://api.stripe.com/v1/payment_intents/${data.paymentIntentId}?client_secret=${data.clientSecret}` 410 ) 411 const retrieveRes = await fetch(url, { 412 method: "GET", 413 headers: { 414 "Content-Type": "application/x-www-form-urlencoded", 415 "Stripe-Account": data.accountId, 416 Authorization: `Bearer ${window.public_stripe_key}` 417 } 418 }) 419 const retrieveData = await retrieveRes.json() 420 if (retrieveData.status === "succeeded") { 421 showToast("Payment was successful", { 422 hasCloseButton: true 423 }) 424 if (data.exteral) { 425 window.location.href = data.exteral 426 } else { 427 try { 428 await redirectNextPage() 429 } catch ({ error }) { 430 showToast("Payment was not successful", { 431 type: "error", 432 hasCloseButton: true 433 }) 434 } 435 } 436 } else { 437 if (retrieveData && retrieveData.invoice) { 438 let invoice = null 439 if (typeof retrieveData.invoice === "string") { 440 invoice = retrieveData.invoice 441 } else if ( 442 typeof retrieveData.invoice === "object" && 443 retrieveData.invoice.id 444 ) { 445 invoice = retrieveData.invoice.id 446 } 447 if (invoice) { 448 const url = new URL( 449 `https://api.stripe.com/v1/invoices/${invoice}/void` 450 ) 451 await fetch(url, { 452 method: "GET", 453 headers: { 454 "Content-Type": "application/x-www-form-urlencoded", 455 "Stripe-Account": data.accountId, 456 Authorization: `Bearer ${window.public_stripe_key}` 457 } 458 }) 459 } 460 } 461 // Authentication failed, prompt the customer to enter another payment method 462 showToast("Payment was not successful", { 463 type: "error", 464 hasCloseButton: true 465 }) 466 } 467 } catch (err) { 468 showToast("Payment was not successful", { 469 type: "error", 470 hasCloseButton: true 471 }) 472 } 473} 474 475function clickDataRef(url, conf) { 476 const videoId = getYoutubeVideoId(url) 477 const vimeoId = getVimeoVideoId(url) 478 if (videoId) { 479 const src = `//www.youtube.com/embed/${videoId}?autoplay=1&rel=0&hd=1&mute=1` 480 openVideoPopup(src, conf) 481 } else if (vimeoId) { 482 const src = `https://player.vimeo.com/video/${vimeoId}?autoplay=1&rel=0&muted=1` 483 openVideoPopup(src, conf) 484 } else { 485 // TODO: check URL is for Image, Video, Youtube, or others 486 openImagePopup(url) 487 } 488} 489 490function getAutoplayAttribute(autoplay) { 491 return autoplay === "yes" ? "allow='autoplay'" : "" 492} 493 494function getAutoplayParam(autoplay, blockPause, paramPrefix) { 495 return autoplay === "yes" || blockPause === "yes" 496 ? `${paramPrefix}autoplay=1` 497 : "autoplay=0" 498} 499 500function updateVideoWidth(element) { 501 const iframe = element.querySelector("iframe") 502 if (!iframe) return 503 const width = Number.isNaN(parseInt(iframe.width, 10)) 504 ? element.clientWidth 505 : parseInt(iframe.width, 10) 506 const height = Number.isNaN(parseInt(iframe.height, 10)) 507 ? width * 0.5625 // 16:9 508 : parseInt(iframe.height, 10) 509 510 element.style.paddingTop = `${(height / width) * 100}%` 511 element.classList.add("fluid-width-video-wrapper") 512} 513 514function handleVideoBlockPause(element, provider) { 515 const playerId = `player_${randomString(10)}` 516 const iframe = element.querySelector("iframe") 517 const unmuteLabel = 518 element.dataset.videoAutoplay === "yes" && 519 element.dataset.videoUnmuteLabel === "yes" && 520 element.dataset.videoControls === "no" 521 if (!iframe) return 522 iframe.id = playerId 523 if (element.dataset.videoBlockPause === "yes" || unmuteLabel) { 524 let player = null 525 const getPlayer = () => { 526 if (provider === "youtube") { 527 if (!window.YT) { 528 setTimeout(getPlayer, 50) 529 } else { 530 player = new window.YT.Player(playerId, {}) 531 } 532 } else if (provider === "vimeo") { 533 if (!window.Vimeo) { 534 setTimeout(getPlayer, 50) 535 } else { 536 player = new window.Vimeo.Player(playerId) 537 } 538 } else if (provider === "wistia") { 539 if (!window._wq) { 540 setTimeout(getPlayer, 50) 541 } else { 542 window._wq.push({ 543 id: playerId, 544 onReady(p) { 545 player = p 546 } 547 }) 548 } 549 } 550 } 551 setTimeout(getPlayer, 50) 552 const playVideoAndUnMute = () => { 553 if (!player) return 554 if (provider === "youtube") { 555 player.playVideo() 556 player.unMute() 557 } else if (provider === "vimeo") { 558 player.play() 559 player.setVolume(1) 560 } else if (provider === "wistia") { 561 player.play() 562 player.unmute() 563 } 564 }
565 const overlay = document.createElement("div") 566 overlay.classList.add("fluid-video-overlay") 567 568 if (element.dataset.videoBlockPause === "yes") { 569 const text = 570 element.dataset.videoStarterText || 571 "Click To Join Session In progress..." 572 const blocker = document.createElement("div") 573 blocker.classList.add("start-session-wrapper") 574 blocker.innerHTML = `<span>${text}</span>` 575 blocker.addEventListener("click", () => { 576 playVideoAndUnMute() 577 blocker.remove() 578 }) 579 overlay.appendChild(blocker) 580 } else if (unmuteLabel) { 581 overlay.innerHTML = "<span><i class='fa fa-volume-up'></i></span>" 582 overlay.addEventListener("click", () => { 583 playVideoAndUnMute() 584 overlay.remove() 585 }) 586 } 587 element.appendChild(overlay) 588 } 589 element.removeAttribute("data-video-block-pause") 590 element.removeAttribute("data-video-controls") 591 element.removeAttribute("data-video-unmute-label") 592 element.removeAttribute("data-video-autoplay") 593 element.removeAttribute("data-video-starter-text") 594} 595 596function loadCustomEmbedVideo(e) { 597 const div = document.createElement("div") 598 div.innerHTML = e.dataset.videoEmbed ?? e.dataset.evpUrl ?? "" 599 updateVideoWidth(div) 600 e.appendChild(div) 601 e.removeAttribute("data-video-embed") 602 e.removeAttribute("data-evp-url") 603} 604 605function loadYoutubeVideoFormURL(element) { 606 const { 607 youtubeUrl, 608 videoAutoplay = "no", 609 videoControls = "yes", 610 videoBlockPause = "no", 611 videoWidth = "640", 612 videoHeight = "360" 613 } = element.dataset 614 const videoId = getYoutubeVideoId(youtubeUrl) 615 const autoplayParam = getAutoplayParam( 616 videoAutoplay, 617 videoBlockPause, 618 "mute=1&" 619 ) 620 const controls = 621 videoBlockPause == "yes" || videoControls === "no" ? "0" : "1" 622 const div = document.createElement("div") 623 div.innerHTML = ` 624 <iframe 625 width="${videoWidth}" 626 height="${videoHeight}" 627 ${getAutoplayAttribute(videoAutoplay)} 628 referrerPolicy="strict-origin-when-cross-origin" 629 src="https://www.youtube.com/embed/${videoId}?${autoplayParam}&controls=${controls}&enablejsapi=1&showinfo=0&rel=0&hd=1&wmode=transparent" 630 frameborder="0" allowfullscreen wmode="opaque"> 631 </iframe> 632 ` 633 updateVideoWidth(div) 634 element.appendChild(div) 635 636 element.removeAttribute("data-youtube-url") 637 handleVideoBlockPause(element, "youtube") 638} 639 640function loadVimeoVideoFromURL(element) { 641 const { 642 vimeoUrl, 643 videoAutoplay = "no", 644 videoBlockPause = "no", 645 videoWidth = "640", 646 videoHeight = "360" 647 } = element.dataset 648 const vimeoId = getVimeoVideoId(vimeoUrl) 649 if (!vimeoId) return 650 651 const div = document.createElement("div") 652 653 const autoplayParam = getAutoplayParam( 654 videoAutoplay, 655 videoBlockPause, 656 "muted=1&" 657 ) 658 div.innerHTML = ` 659 <iframe width="${videoWidth}" 660 height="${videoHeight}" 661 ${getAutoplayAttribute(videoAutoplay)} 662 src="https://player.vimeo.com/video/${vimeoId}?${autoplayParam}&title=0&byline=0&wmode=transparent&autopause=0" 663 frameborder="0" allowfullscreen wmode="opaque"></iframe>` 664 665 updateVideoWidth(div) 666 element.appendChild(div) 667 element.removeAttribute("data-vimeo-url") 668 handleVideoBlockPause(element, "vimeo") 669} 670 671function loadWistiaVideoFromURL(element) { 672 const { 673 wistiaUrl = "", 674 videoAutoplay = "no", 675 videoBlockPause = "no", 676 videoWidth = "640", 677 videoHeight = "360" 678 } = element.dataset 679 const wistiaId = wistiaUrl.split("/").pop() 680 if (!wistiaId) return 681 const div = document.createElement("div") 682 const autoplayParam = getAutoplayParam( 683 videoAutoplay, 684 videoBlockPause, 685 "muted=true&" 686 ) 687 div.innerHTML = ` 688 <iframe width="${videoWidth}" 689 height="${videoHeight}" 690 ${getAutoplayAttribute(videoAutoplay)} 691 src="//fast.wistia.net/embed/iframe/${wistiaId}?${autoplayParam}&wmode=transparent" 692 allowtransparency="true" frameborder="0" scrolling="no" 693 class="wistia_embed" name="wistia_embed" 694 allowfullscreen mozallowfullscreen webkitallowfullscreen oallowfullscreen msallowfullscreen wmode="opaque"></iframe>` 695 updateVideoWidth(div) 696 element.appendChild(div) 697 element.removeAttribute("data-wistia-url") 698 handleVideoBlockPause(element, "wistia") 699} 700 701function loadHTML5Video(element) { 702 const { mp4Url, webmUrl, posterUrl } = element.dataset 703 if (!mp4Url || !webmUrl) return 704 705 function startHTML5Video() { 706 const div = document.createElement("div") 707 div.classList.add("fluid-width-video-wrapper") 708 div.style.paddingTop = "56.25%" 709 const playerId = `player_${randomString(10)}` 710 const posterAttribute = posterUrl ? ` poster="${posterUrl}" ` : "" 711 712 div.innerHTML = ` 713 <video id="${playerId}" preload="auto" ${posterAttribute} style="position: absolute; top: 0px; left: 0px;"> 714 <source src="${mp4Url}" type="video/mp4"> 715 <source src="${webmUrl}" type="video/webm"> 716 Your browser does not support HTML5 video. 717 </video>` 718 719 element.removeAttribute("data-mp4-url") 720 element.removeAttribute("data-webm-url") 721 element.removeAttribute("data-poster-url") 722 element.appendChild(div) 723 724 new window.MediaElementPlayer(playerId, { 725 stretching: "auto", 726 videoWidth: "100%", 727 videoHeight: "100%", 728 iconSprite: "/images/mejs-controls.svg", 729 features: [ 730 "playpause", 731 "current", 732 "progress", 733 "duration", 734 "volume" 735 ], 736 style: "position: absolute; top: 0px; left: 0px;" 737 }) 738 } 739 740 checkMEPlayerAvailable(startHTML5Video) 741} 742 743function loadAudio(element) { 744 const { 745 audioUrl = "",
746 audioControls = "yes", 747 audioAutoplay, 748 audioLoop 749 } = element.dataset 750 const playerId = `audio_player_${randomString(10)}` 751 752 element.innerHTML = ` 753 <div class="fluid-width-video-wrapper" style="height:40px;"> 754 <audio id=${playerId} src="${audioUrl}"></audio> 755 </div> 756 ` 757 const audioAttributes = { 758 src: audioUrl, 759 controls: audioControls !== "no", 760 loop: audioLoop === "yes", 761 autoplay: audioAutoplay === "yes" 762 } 763 764 let features = ["playpause", "current", "progress", "duration", "volume"] 765 if (!audioAttributes.controls) { 766 features = ["progress", "volume"] 767 } 768 769 const playerOptions = { 770 audioWidth: "100%", 771 audioHeight: "100%", 772 enableAutosize: true, 773 enableProgressTooltip: false, 774 enableKeyboard: audioAttributes.controls, 775 features, 776 loop: audioAttributes.loop, 777 iconSprite: "/images/mejs-controls.svg", 778 success: audioAttributes.autoplay 779 ? (mediaElement) => mediaElement.play() 780 : undefined 781 } 782 783 element.removeAttribute("data-audio-url") 784 element.removeAttribute("data-audio-controls") 785 element.removeAttribute("data-audio-autoplay") 786 element.removeAttribute("data-audio-loop") 787 new window.MediaElementPlayer(playerId, playerOptions) 788} 789 790function showToast( 791 message, 792 { type = "success", duration = 4000, hasCloseButton = false } 793) { 794 const toast = document.createElement("div") 795 796 toast.innerHTML = `<div>${message}</div>` 797 toast.className = `toast ${type}` 798 799 if (hasCloseButton) { 800 const closeIcon = document.createElement("i") 801 closeIcon.className = "fa fa-close toastCloseIcon" 802 803 closeIcon.onclick = () => { 804 toast.classList.add("hideToast") 805 setTimeout(() => toast.remove(), 500) 806 } 807 808 toast.appendChild(closeIcon) 809 } 810 811 document.body.appendChild(toast) 812 813 if (!hasCloseButton) { 814 toast.timeoutId = setTimeout(() => { 815 toast.classList.add("hideToast") 816 817 if (toast.timeoutId) clearTimeout(toast.timeoutId) 818 819 setTimeout(() => toast.remove(), 500) 820 }, duration) 821 } 822} 823 824async function sendRequest(endpoint, options) { 825 const parsedUrl = new URL(window.location.href) 826 const apiUrl = `${parsedUrl.protocol}//${parsedUrl.host}` 827 828 const response = await fetch(`${apiUrl}/${endpoint}`, options) 829 const data = await response.json() 830 if (!response.ok) throw data 831 return data 832} 833 834async function redirectNextPage({ ocuSubmit = false } = {}) { 835 const url = window.location.href 836 const response = await sendRequest("funnels/nextPage", { 837 method: "POST", 838 headers: { "Content-Type": "application/json" }, 839 body: JSON.stringify({ url }) 840 }) 841 842 if (ocuSubmit) { 843 const res = await fetch(response.contentUrl) 844 const content = await res.text() 845 if (!content?.includes("#yes-link")) { 846 clearCookie("paymentId") 847 clearCookie("customerId") 848 } 849 } 850 851 // Redirect the next page 852 window.location.href = 853 url.substring(0, url.lastIndexOf("/")) + "/" + (response.path || "") 854} 855 856async function isExistRedirectURL() { 857 const url = window.location.href 858 const response = await sendRequest("funnels/currentPage", { 859 method: "POST", 860 headers: { "Content-Type": "application/json" }, 861 body: JSON.stringify({ url }) 862 }) 863 if (response && response.metadata && response.metadata.redirectURL) { 864 return response.metadata.redirectURL 865 } 866 return 867} 868 869// eslint-disable-next-line 870function onYouTubePlayerAPIReady() { 871 // enable video background 872 loadVideoBackground() 873 // load Youtube videos 874 for (const element of document.querySelectorAll("[data-youtube-url]")) { 875 loadYoutubeVideoFormURL(element) 876 } 877} 878 879function saveSessionId(type, sessionId) { 880 const now = new Date() 881 const expirationTime = now.getTime() + 30 * 24 * 60 * 60 * 1000 882 const sessionData = { 883 value: sessionId, 884 expiration: expirationTime 885 } 886 887 localStorage.setItem(type, JSON.stringify(sessionData)) 888} 889 890function checkSessionValid(type) { 891 const data = localStorage.getItem(type) 892 if (!data) return null 893 894 const json = JSON.parse(data) 895 if (!json.value || !json.expiration) return null 896 897 const now = new Date() 898 if (now.getTime() > json.expiration) return null 899 900 return json.value 901} 902 903function getCookie(name) { 904 const value = `; ${document.cookie}` 905 const parts = value.split(`; ${name}=`) 906 if (parts.length === 2) { 907 return parts.pop().split(";").shift() 908 } 909 return null 910} 911 912function clearCookie(name) { 913 document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;` 914} 915 916document.addEventListener("DOMContentLoaded", function () { 917 // load youtube iframe_api if the video background enabled
918 const videoBackgroundElement = document.querySelector( 919 "[data-video-background]" 920 ) 921 922 if (videoBackgroundElement) { 923 try { 924 const data = JSON.parse( 925 videoBackgroundElement.dataset.videoBackground 926 ) 927 const videoType = data.videoType 928 929 if (videoType === "youtube") { 930 injectYoutubeScript() 931 } else { 932 loadVideoBackground() 933 } 934 } catch (error) { 935 console.warn("invalid video background settings.") 936 return 937 } 938 } 939 940 const videoBackgroundElements = document.querySelectorAll( 941 "[data-background-video-type]" 942 ) 943 944 videoBackgroundElements?.forEach(async (element) => { 945 const type = element.dataset.backgroundVideoType 946 947 element.style.position = "relative" 948 949 const player = document.createElement("div") 950 const playerId = `player_${randomString(10)}` 951 player.id = playerId 952 953 player.style.position = "absolute" 954 player.style.top = "0" 955 player.style.left = "0" 956 player.style.bottom = "0" 957 player.style.right = "0" 958 player.style.width = "100%" 959 player.style.height = "100%" 960 player.style.pointerEvents = "none" 961 962 element.insertBefore(player, element.firstChild) 963 964 if (type === "youtube") { 965 const url = element.dataset.backgroundYoutubeUrl 966 const videoId = getYoutubeVideoId(url) 967 968 if (!videoId) return 969 970 await initializeYouTubePlayer(playerId, videoId) 971 } else if (type === "vimeo") { 972 const url = element.dataset.backgroundVimeoUrl 973 const videoId = getVimeoVideoId(url) 974 975 if (!videoId) return 976 977 const vimeoPlayer = await initializeVimeoPlayer(playerId, videoId) 978 979 vimeoPlayer.on("loaded", () => { 980 vimeoPlayer.play() 981 const playerElement = vimeoPlayer.element 982 playerElement.style.width = "100%" 983 playerElement.style.height = "100%" 984 }) 985 } else if (type === "wistia") { 986 const url = element.dataset.backgroundWistiaUrl 987 const videoId = getWistiaVideoId(url) 988 989 await initializeWistiaPlayer(playerId, videoId, { 990 onReady: (video) => { 991 video.bind("end", function () { 992 video.play() 993 }) 994 } 995 }) 996 } 997 }) 998 999 const aos = window.AOS 1000 1001 if (aos) { 1002 /** 1003 * available options are: 1004 * https://github.com/michalsnik/aos?tab=readme-ov-file#1-initialize-aos 1005 */ 1006 aos.init({}) 1007 /** 1008 * in this way we can initiate AOS animation on page load 1009 * otherwise animation will be triggered on scroll page. 1010 */ 1011 } 1012 1013 const loadElements = document.querySelectorAll("[data-animation='load']") 1014 for (const element of loadElements) { 1015 element.classList.add("aos-animate") 1016 } 1017 1018 // "data-ref" attribute: show a popup 1019 for (const element of document.querySelectorAll("[data-ref]")) { 1020 const reference = element.dataset.ref 1021 const videoTitle = element.dataset.videoTitle 1022 if (reference) { 1023 const clickEvent = () => clickDataRef(reference, { videoTitle }) 1024 1025 if ( 1026 element.classList.contains("image-popup-wrapper") || 1027 element.classList.contains("video-popup-wrapper") 1028 ) { 1029 for (const img of element.querySelectorAll("img")) { 1030 img.tabIndex = "0" 1031 img.addEventListener("click", clickEvent) 1032 } 1033 } else { 1034 element.tabIndex = "0" 1035 element.addEventListener("click", clickEvent) 1036 } 1037 } 1038 } 1039 1040 // "data-video-type" attribute: play video 1041 for (const element of document.querySelectorAll("[data-video-type]")) { 1042 const videoType = element.dataset.videoType 1043 if (videoType === "embed" || videoType === "evp") { 1044 loadCustomEmbedVideo(element) 1045 } else if (videoType === "youtube") { 1046 injectYoutubeScript() 1047 } else if (videoType === "vimeo") { 1048 injectVimeoScript() 1049 loadVimeoVideoFromURL(element) 1050 } else if (videoType === "wistia") { 1051 injectWistiaScript() 1052 loadWistiaVideoFromURL(element) 1053 } else if (videoType === "html5") { 1054 loadHTML5Video(element) 1055 } else { 1056 console.log("Unsupported Video Type: ", videoType) 1057 } 1058 element.removeAttribute("data-video-type") 1059 } 1060 1061 // ------------- Accordions ------------- 1062 const accordions = document.querySelectorAll(".accordion-block") 1063 1064 accordions?.forEach((acc) => { 1065 const header = acc.querySelector(".accordion-block-header") 1066 header.addEventListener("click", function () { 1067 acc.classList.toggle("open") 1068 1069 const body = acc.querySelector(".accordion-block-body") 1070 const icon = acc.querySelector(".fa") 1071 1072 if (body.style.maxHeight) { 1073 body.style.maxHeight = null 1074 icon.className = "fa fa-plus" 1075 } else { 1076 body.style.maxHeight = body.scrollHeight + "px" 1077 icon.className = "fa fa-minus" 1078 } 1079 }) 1080 }) 1081 1082 // "data-audio-url" attribute: play audio 1083 for (const element of document.querySelectorAll("[data-audio-url]")) { 1084 checkMEPlayerAvailable(() => loadAudio(element)) 1085 } 1086 1087 // ------------- element & video sticky start ------------- 1088 // sticky elements 1089 const stickyElement = [] 1090 for (const element of document.querySelectorAll(".sticky-top")) { 1091 const containerId = `sticky-container-${randomString(10)}` 1092 const div = document.createElement("div") 1093 div.id = containerId 1094 div.style.height = `${element.clientHeight}px` 1095 element.parentNode.insertBefore(div, element) 1096 div.appendChild(element) 1097 1098 stickyElement.push(containerId) 1099 } 1100 1101 const stickyVideos = []
1102 for (const e of document.querySelectorAll("[data-video-sticky='yes']")) { 1103 let id = e.id 1104 if (!id) { 1105 id = randomString(12) 1106 e.id = id 1107 } 1108 const { 1109 videoStickyWidth, 1110 videoStickyPosition, 1111 videoStickyTheme, 1112 videoStickyClose 1113 } = e.dataset 1114 stickyVideos.push({ 1115 videoStickyId: id, 1116 videoStickyWidth: videoStickyWidth ?? "250", 1117 videoStickyPosition: videoStickyPosition ?? "top-right", 1118 videoStickyTheme: videoStickyTheme ?? "da-sticky-video-theme1", 1119 videoStickyClose: videoStickyClose ?? "none" 1120 }) 1121 e.removeAttribute("data-video-sticky") 1122 e.removeAttribute("data-video-sticky-width") 1123 e.removeAttribute("data-video-sticky-position") 1124 e.removeAttribute("data-video-sticky-theme") 1125 e.removeAttribute("data-video-sticky-close") 1126 } 1127 1128 function scrollHandler() { 1129 for (const data of stickyVideos) { 1130 const { videoStickyId } = data 1131 const element = document.getElementById(videoStickyId) 1132 if (element) { 1133 const rect = element.getBoundingClientRect() 1134 if (rect.bottom < 0) { 1135 stickyVideo(element, data, "add") 1136 } else { 1137 stickyVideo(element, data, "remove") 1138 } 1139 } 1140 } 1141 1142 for (const stickyId of stickyElement) { 1143 const container = document.getElementById(stickyId) 1144 const element = container?.querySelector(".sticky-top") 1145 if (element && container) { 1146 const rect = container.getBoundingClientRect() 1147 if (rect.top < 0) { 1148 element.style.position = "fixed" 1149 } else { 1150 element.style.position = "" 1151 } 1152 } 1153 } 1154 } 1155 1156 function stickyVideo(e, d, m) { 1157 const iframe = e.querySelector("iframe") 1158 if (!iframe) return 1159 const { 1160 videoStickyPosition, 1161 videoStickyTheme, 1162 videoStickyClose, 1163 videoStickyWidth 1164 } = d 1165 iframe.classList[m]( 1166 "sticky-video", 1167 videoStickyTheme, 1168 videoStickyPosition, 1169 `sticky-video-${videoStickyWidth}` 1170 ) 1171 for (const element of e.querySelectorAll(".sticky-video-close")) { 1172 element.remove() 1173 } 1174 if (videoStickyClose === "yes" && m === "add") { 1175 const btn = document.createElement("button") 1176 btn.innerHTML = "<i class='fa fa-times'></i>" 1177 btn.classList.add( 1178 "sticky-video-close", 1179 videoStickyTheme, 1180 videoStickyPosition, 1181 `sticky-video-${videoStickyWidth}` 1182 ) 1183 btn.addEventListener("click", () => { 1184 stickyVideo(e, d, "remove") 1185 window.removeEventListener("scroll", scrollHandler) 1186 }) 1187 e.appendChild(btn) 1188 } 1189 } 1190 1191 if (stickyVideos.length > 0 || stickyElement.length) { 1192 window.addEventListener("scroll", scrollHandler) 1193 } 1194 // ------------- video sticky end ------------- 1195}) 1196 1197if (window.$) { 1198 let shouldShowExitPopup = true 1199 1200 function showGlobalPopup() { 1201 $(".gjs-popup").show() 1202 $(".gjs-popup").addClass("enter-popup") 1203 1204 $(".gjs-popup-close, .gjs-popup-backdrop").on("click", function () { 1205 $(".gjs-popup").removeClass("enter-popup") 1206 $(".gjs-popup").addClass("exit-popup") 1207 1208 setTimeout(() => { 1209 $(".gjs-popup").hide() 1210 $(".gjs-popup").removeClass("exit-popup") 1211 }, 300) 1212 }) 1213 } 1214 1215 $(document).on("mouseout", (event) => { 1216 const $exitPopupTrigger = $("[data-trigger-on-exit='bounce']") 1217 1218 if ( 1219 !event.relatedTarget && 1220 !event.toElement && 1221 $exitPopupTrigger?.length && 1222 shouldShowExitPopup 1223 ) { 1224 shouldShowExitPopup = false 1225 showGlobalPopup() 1226 } 1227 }) 1228 1229 // ------------- phone number element ------------- 1230 const phoneElement = $("[name='phone']") 1231 1232 if (phoneElement.length) { 1233 const countryCodeElement = $("[name='country_codes_phone']") 1234 const MIN_PHONE_DIGITS = 8 1235 1236 function formatPhoneNumber(value, countryCode) { 1237 try { 1238 const phoneNumber = new libphonenumber.parsePhoneNumber( 1239 value, 1240 countryCode || "US" 1241 ) 1242 1243 return phoneNumber.formatNational() 1244 } catch (error) { 1245 console.error("Error formatting phone number:", error) 1246 return value 1247 } 1248 } 1249 1250 countryCodeElement.on("change", function () { 1251 const selectedValue = $(this).val() 1252 1253 phoneElement.val( 1254 formatPhoneNumber(phoneElement.val(), selectedValue) 1255 ) 1256 }) 1257 1258 phoneElement.on("input", function () { 1259 const inputValue = $(this).val() 1260 const countryCodeValue = countryCodeElement?.val() 1261 const digitsOnly = inputValue.replace(/\D/g, "") 1262 1263 if (digitsOnly?.length >= MIN_PHONE_DIGITS) { 1264 $(this).val(formatPhoneNumber(inputValue, countryCodeValue)) 1265 } 1266 }) 1267 } 1268 1269 // ------------- customJs elements ------------- 1270 $("[data-custom-js]").each(function () { 1271 const customJs = $(this).attr("data-custom-js") 1272 const type = $(this).attr("data-displaytype") 1273 type === "postscribe" 1274 ? window?.postscribe(this, customJs) 1275 : $(this).html(customJs) 1276 $(this).removeAttr("data-custom-js") 1277 }) 1278 1279 // ------------- countdown timers ----------- 1280 $(function () { 1281 function onExpireTimers() { 1282 const action = $(this).attr("data-expire-type") 1283 1284 if (action === "showhide") { 1285 const hide = $(this).attr("data-hide-ids") 1286 const show = $(this).attr("data-show-ids") 1287 if (hide) { 1288 $.each(hide.split(","), function () { 1289 $(`#${this}`).hide() 1290 }) 1291 } 1292 if (show) { 1293 $.each(show.split(","), function () { 1294 $(`#${this}`).show() 1295 }) 1296 } 1297 } else if (action === "url") { 1298 const url = $(this).attr("data-url") 1299 if (url) window.location.href = url 1300 } 1301 } 1302 1303 function onTickUpdated() { 1304 const updateColor = (selector, attr) => { 1305 const color = $(this).attr(attr) 1306 if (color) $(this).find(selector).css("color", color) 1307 } 1308 updateColor(".countdown-amount", "data-color-time") 1309 updateColor(".countdown-period", "data-color-label") 1310 } 1311 1312 function updateExpireCookie(e, date, zone) { 1313 const cookie = window.Cookies 1314 const action = $(e).attr("data-revisit-action") 1315 const days = $(e).attr("data-expire-cookie") 1316 const id = $(e).attr("id") 1317 1318 if ($(e).attr("data-expire-type") === "showhide") { 1319 const show = $(e).attr("data-show-ids") 1320 if (show) { 1321 $.each(show.split(","), function () { 1322 $(`#${this}`).hide() 1323 }) 1324 } 1325 } 1326 if (action === "newdiv") { 1327 const show = $(e).attr("data-show-ids-extra") 1328 if (show) { 1329 $.each(show.split(","), function () { 1330 $(`#${this}`).hide() 1331 }) 1332 } 1333 } 1334 if (cookie) { 1335 if (action === "reset") { 1336 cookie.remove(`da-eg-timer-${id}`) 1337 cookie.remove(`da-eg-timer-nd-${id}`) 1338 } 1339 if (cookie.get(`da-eg-timer-${id}`)) { 1340 // expire/don't run the countdown until re-visit for x days.
1341 date = 0 1342 zone = 0 1343 } else if (cookie.get(`da-eg-timer-nd-${id}`)) { 1344 date = 0 1345 zone = 0 1346 $(e).hide() 1347 1348 const hide = $(e).attr("data-hide-ids") 1349 const show = $(e).attr("data-show-ids") 1350 const showExtra = $(e).attr("data-show-ids-extra") 1351 if (hide) { 1352 $.each(hide.split(","), function () { 1353 $(`#${this}`).hide() 1354 }) 1355 } 1356 if (show) { 1357 $.each(show.split(","), function () { 1358 $(`#${this}`).hide() 1359 }) 1360 } 1361 if (showExtra) { 1362 $.each(showExtra.split(","), function () { 1363 $(`#${this}`).show() 1364 }) 1365 } 1366 } 1367 1368 if (action === "expirecookie") { 1369 if (!cookie.get(`da-eg-timer-${id}`) && days) { 1370 cookie.set(`da-eg-timer-${id}`, "x", { 1371 expires: parseInt(days) 1372 }) 1373 } 1374 cookie.remove(`da-eg-timer-nd-${id}`) 1375 } else if (action === "newdiv") { 1376 if (days && !cookie.get(`da-eg-timer-${id}`)) { 1377 cookie.set(`da-eg-timer-nd-${id}`, "x", { 1378 expires: parseInt(days) 1379 }) 1380 } 1381 cookie.remove(`da-eg-timer-${id}`) 1382 } 1383 } 1384 return [date, zone] 1385 } 1386 1387 // prettier-ignore 1388 const locals = { 1389 ar: { 1390 labels: ["سÙÙØ§Øª", "Ø£Ø´ÙØ±", "Ø£Ø³Ø§Ø¨ÙØ¹", "Ø£ÙØ§Ù ", "ساعات", "Ø¯ÙØ§Ø¦Ù", "Ø«ÙØ§ÙÙ"], 1391 labels1: ["Ø³ÙØ©", "Ø´ÙØ±", "Ø£Ø³Ø¨ÙØ¹", "ÙÙÙ ", "ساعة", "دÙÙÙØ©", "ثاÙÙØ©"], 1392 compactLabels: ["س", "Ø´", "Ø£", "Ù"], 1393 whichLabels: null, 1394 digits: ["Ù ", "Ù¡", "Ù¢", "Ù£", "Ù¤", "Ù¥", "Ù¦", "Ù§", "Ù¨", "Ù©"], 1395 timeSeparator: ":", 1396 isRTL: true 1397 }, 1398 de: { 1399 labels: ["Jahre", "Monate", "Wochen", "Tage", "Stunden", "Minuten", "Sekunden"], 1400 labels1: ["Jahr", "Monat", "Woche", "Tag", "Stunde", "Minute", "Sekunde"], 1401 compactLabels: ["J", "M", "W", "T"], 1402 whichLabels: null, 1403 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1404 timeSeparator: ":", 1405 isRTL: false 1406 }, 1407 en: { 1408 labels: ["Years", "Months", "Weeks", "Days", "Hours", "Minutes", "Seconds"], 1409 labels1: ["Year", "Month", "Week", "Day", "Hour", "Minute", "Second"], 1410 compactLabels: ["y", "m", "w", "d"], 1411 whichLabels: null, 1412 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1413 timeSeparator: ":", 1414 isRTL: false 1415 }, 1416 es: { 1417 labels: ["Años", "Meses", "Semanas", "DÃas", "Horas", "Minutos", "Segundos"], 1418 labels1: ["Año", "Mes", "Semana", "DÃa", "Hora", "Minuto", "Segundo"], 1419 compactLabels: ["a", "m", "s", "d"], 1420 whichLabels: null, 1421 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1422 timeSeparator: ":", 1423 isRTL: false 1424 }, 1425 fr: { 1426 labels: ["Années", "Mois", "Semaines", "Jours", "Heures", "Minutes", "Secondes"], 1427 labels1: ["Année", "Mois", "Semaine", "Jour", "Heure", "Minute", "Seconde"], 1428 compactLabels: ["a", "m", "s", "j"], 1429 whichLabels: function (amount) { 1430 return amount > 1 ? 0 : 1 1431 }, 1432 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1433 timeSeparator: ":", 1434 isRTL: false 1435 }, 1436 it: { 1437 labels: ["Anni", "Mesi", "Settimane", "Giorni", "Ore", "Minuti", "Secondi"], 1438 labels1: ["Anno", "Mese", "Settimana", "Giorno", "Ora", "Minuto", "Secondo"], 1439 compactLabels: ["a", "m", "s", "g"], 1440 whichLabels: null, 1441 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1442 timeSeparator: ":", 1443 isRTL: false 1444 }, 1445 ja: { 1446 labels: ["å¹´", "æ", "é±", "æ¥", "æ", "å", "ç§"], 1447 labels1: ["å¹´", "æ", "é±", "æ¥", "æ", "å", "ç§"], 1448 compactLabels: ["å¹´", "æ", "é±", "æ¥"], 1449 whichLabels: null, 1450 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1451 timeSeparator: ":", 1452 isRTL: false 1453 }, 1454 ko: { 1455 labels: ["ë ", "ì", "주", "ì¼", "ì", "ë¶", "ì´"], 1456 labels1: ["ë ", "ì", "주", "ì¼", "ì", "ë¶", "ì´"], 1457 compactLabels: ["ë ", "ì", "주", "ì¼"], 1458 compactLabels1: ["ë ", "ì", "주", "ì¼"], 1459 whichLabels: null, 1460 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1461 timeSeparator: ":", 1462 isRTL: false 1463 }, 1464 ru: { 1465 labels: ["ÐеÑ", "ÐеÑÑÑев", "ÐеделÑ", "Ðней", "ЧаÑов", "ÐинÑÑ", "СекÑнд"], 1466 labels1: ["Ðод", "ÐеÑÑÑ", "ÐеделÑ", "ÐенÑ", "ЧаÑ", "ÐинÑÑа", "СекÑнда"], 1467 labels2: ["Ðода", "ÐеÑÑÑа", "Ðедели", "ÐнÑ", "ЧаÑа", "ÐинÑÑÑ", "СекÑндÑ"], 1468 compactLabels: ["л", "м", "н", "д"], 1469 compactLabels1: ["г", "м", "н", "д"], 1470 whichLabels: function (amount) { 1471 var units = amount % 10 1472 var tens = Math.floor((amount % 100) / 10) 1473 return amount === 1 1474 ? 1 1475 : units >= 2 && units <= 4 && tens !== 1 1476 ? 2 1477 : units === 1 && tens !== 1 1478 ? 1 1479 : 0 1480 }, 1481 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1482 timeSeparator: ":", 1483 isRTL: false 1484 }, 1485 sv: { 1486 labels: ["à r", "MÃ¥nader", "Veckor", "Dagar", "Timmar", "Minuter", "Sekunder"], 1487 labels1: ["à r", "MÃ¥nad", "Vecka", "Dag", "Timme", "Minut", "Sekund"], 1488 compactLabels: ["à ", "M", "V", "D"], 1489 whichLabels: null, 1490 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1491 timeSeparator: ":", 1492 isRTL: false 1493 }, 1494 "zh-CN": { 1495 labels: ["å¹´", "æ", "å¨", "天", "æ¶", "å", "ç§"], 1496 labels1: ["å¹´", "æ", "å¨", "天", "æ¶", "å", "ç§"], 1497 compactLabels: ["å¹´", "æ", "å¨", "天"], 1498 compactLabels1: ["å¹´", "æ", "å¨", "天"], 1499 whichLabels: null, 1500 digits: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 1501 timeSeparator: ":", 1502 isRTL: false 1503 } 1504 } 1505 1506 // ------------- minutes timer elements ----------- 1507 $(".gjs-minute-timer-block").each(function () { 1508 const hours = $(this).attr("data-hours") ?? "0" 1509 const minutes = $(this).attr("data-minutes") ?? "0" 1510 const seconds = $(this).attr("data-seconds") ?? "0" 1511 const lang = $(this).attr("data-lang") 1512 let secs = 1513 parseInt(hours) * 3600 + 1514 parseInt(minutes) * 60 + 1515 parseInt(seconds) 1516 1517 const [countdownDate] = updateExpireCookie(this, secs, 0) 1518 1519 $(this).countdown( 1520 $.extend( 1521 { 1522 until: countdownDate, 1523 padZeroes: true, 1524 alwaysExpire: true, 1525 onExpiry: onExpireTimers 1526 }, 1527 locals[lang] ?? locals.en 1528 ) 1529 ) 1530 }) 1531 1532 // ------------- daily timer elements ----------- 1533 $(".gjs-day-timer-block").each(function () { 1534 // eslint-disable-next-line no-undef 1535 let date = moment().format("MM/DD/YYYY") 1536 date = date.split("/") 1537 const time = $(this).attr("data-time") ?? "00"
1538 const tz = $(this).attr("data-tz") ?? "America/New_York" 1539 const lang = $(this).attr("data-lang") ?? "en" 1540 date = new Date(date[2], date[0] - 1, date[1], time, 0, 0) 1541 // eslint-disable-next-line no-undef 1542 let zone = moment.tz(date, tz).utcOffset() / 60 1543 1544 const [countdownDate, timezone] = updateExpireCookie( 1545 this, 1546 date, 1547 zone 1548 ) 1549 1550 $(this).countdown( 1551 $.extend( 1552 { 1553 until: countdownDate, 1554 timezone: timezone, 1555 padZeroes: true, 1556 alwaysExpire: true, 1557 onExpiry: onExpireTimers 1558 }, 1559 locals[lang] ?? locals.en 1560 ) 1561 ) 1562 }) 1563 1564 // ------------- countdown 2.0 elements ----------- 1565 $(".gjs-countdown-block").each(function () { 1566 let date = $(this).attr("data-date") 1567 const time = $(this).attr("data-time") ?? "00" 1568 let tz = $(this).attr("data-tz") ?? "America/New_York" 1569 const lang = $(this).attr("data-lang") ?? "en" 1570 if (!date) return 1571 date = date.split("/") 1572 1573 date = new Date(date[2], date[0] - 1, date[1], time, 0, 0) 1574 // eslint-disable-next-line no-undef 1575 tz = moment.tz(date, tz).utcOffset() / 60 1576 1577 if ($(this).attr("data-expire-type") === "showhide") { 1578 const show = $(this).attr("data-show-ids") 1579 if (show) { 1580 $.each(show.split(","), function () { 1581 $(`#${this}`).hide() 1582 }) 1583 } 1584 } 1585 1586 let f = "" 1587 if ($(this).attr("data-show-years") === "true") f = `${f}y` 1588 if ($(this).attr("data-show-months") === "true") f = `${f}o` 1589 if ($(this).attr("data-show-weeks") === "true") f = `${f}w` 1590 if ($(this).attr("data-show-days") === "true") f = `${f}d` 1591 if ($(this).attr("data-show-hours") === "true") f = `${f}H` 1592 if ($(this).attr("data-show-minutes") === "true") f = `${f}M` 1593 if ($(this).attr("data-show-seconds") === "true") f = `${f}S` 1594 1595 $(this).countdown( 1596 $.extend( 1597 { 1598 format: f, 1599 until: date, 1600 timezone: tz, 1601 padZeroes: true, 1602 alwaysExpire: true, 1603 onExpiry: onExpireTimers, 1604 onTick: onTickUpdated 1605 }, 1606 locals[lang] ?? locals.en 1607 ) 1608 ) 1609 }) 1610 }) 1611 1612 // ------------- form or page submission and data-url ----------- 1613 $(function () { 1614 let isSubmitting = false 1615 let isDirty = false 1616 1617 async function submitData(data, returnUrl) { 1618 try { 1619 const url = window.location.href 1620 const globalRedirectUrl = 1621 returnUrl || window.redirect_url_after_submit 1622 1623 data.url = url 1624 await sendRequest("contactsNew", { 1625 method: "POST", 1626 headers: { "Content-Type": "application/json" }, 1627 body: JSON.stringify(data) 1628 }) 1629 1630 const redirectURL = await isExistRedirectURL() 1631 if (redirectURL) { 1632 window.location.href = redirectURL 1633 } 1634 1635 if (!globalRedirectUrl) { 1636 redirectNextPage() 1637 } else { 1638 window.location.href = globalRedirectUrl 1639 } 1640 } catch (error) { 1641 console.error(error) 1642 } 1643 } 1644 1645 function redirectAfterSubmit() { 1646 if (window.redirect_url_after_submit) { 1647 window.location.href = window.redirect_url_after_submit 1648 } 1649 } 1650 1651 async function onSubmitPage(returnUrl) { 1652 if (isSubmitting) return 1653 1654 const values = {} 1655 let valid = true 1656 isDirty = true 1657 1658 $(".elInput:not(.elOrder2StepWrapper .elInput)").each(function () { 1659 const name = $(this).prop("name") 1660 const isCheckbox = $(this).attr("type") === "checkbox" 1661 const value = isCheckbox 1662 ? $(this).is(":checked") 1663 : $(this).val() 1664 const required = $(this).prop("required") 1665 if (required && !value) { 1666 valid = false 1667 if (isCheckbox) { 1668 $(this).css("outline", "1px solid #b91517")
1669 $(this).css("outline-offset", "-1px") 1670 } else { 1671 $(this).css("border-color", "#b91517") 1672 } 1673 } else if (name && value !== "") { 1674 values[name] = value 1675 } 1676 1677 if (name === "phone" && value?.length) { 1678 try { 1679 const parsedPhone = new libphonenumber.parsePhoneNumber( 1680 value, 1681 values?.country_codes_phone || "US" 1682 ) 1683 1684 if (!parsedPhone.isPossible()) { 1685 throw new Error("Invalid phone number") 1686 } 1687 } catch (error) { 1688 $(this).css("border-color", "#b91517") 1689 console.error("Error validating phone number:", error) 1690 } 1691 } 1692 }) 1693 let data = {} 1694 let addr = {} 1695 let shippingArr = {} 1696 1697 if ("address" in values) addr["addressFullName"] = values.address 1698 if ("city" in values) addr["addressCity"] = values.city 1699 if ("country" in values) addr["addressCountry"] = values.country 1700 if ("ca_province" in values) 1701 addr["addressState"] = values.ca_province 1702 if ("us_state" in values) addr["addressState"] = values.us_state 1703 if ("state" in values) addr["addressState"] = values.state 1704 if ("zip" in values) addr["addressZipCode"] = values.zip 1705 1706 if ("billing_address" in values) 1707 addr["addressFullName"] = values.billing_address 1708 if ("billing_city" in values) 1709 addr["addressCity"] = values.billing_city 1710 if ("billing_country" in values) 1711 addr["addressCountry"] = values.billing_country 1712 if ("billing_state" in values) 1713 addr["addressState"] = values.billing_state 1714 if ("billing_zip" in values) 1715 addr["addressZipCode"] = values.billing_zip 1716 1717 if ("shipping_address" in values) 1718 shippingArr["shippingAddressFullName"] = values.shipping_address 1719 if ("shipping_city" in values) 1720 shippingArr["shippingAddressCity"] = values.shipping_city 1721 if ("shipping_country" in values) 1722 shippingArr["shippingAddressCountry"] = values.shipping_country 1723 if ("shipping_state" in values) 1724 shippingArr["shippingAddressState"] = values.shipping_state 1725 if ("shipping_zip" in values) 1726 shippingArr["shippingAddressZipCode"] = values.shipping_zip 1727 1728 if ("first_name" in values || "last_name" in values) { 1729 data.name = `${values.first_name || ""} ${ 1730 values.last_name || "" 1731 }` 1732 } 1733 if ("name" in values) data.name = values.name 1734 if ("email" in values) data.email = values.email 1735 if ("phone" in values) { 1736 try { 1737 const parsedPhone = new libphonenumber.parsePhoneNumber( 1738 values.phone, 1739 values?.country_codes_phone || "US" 1740 ) 1741 if (!parsedPhone.isPossible()) { 1742 throw new Error("Invalid phone number") 1743 } 1744 1745 data.phoneNumber = parsedPhone?.number 1746 } catch (error) { 1747 console.error("Error parsing phone number:", error) 1748 valid = false 1749 } 1750 } 1751 if ("sms_approved" in values) 1752 data.unsubscribedSMS = !values.sms_approved 1753 if (Object.keys(addr).length > 0) data.address = addr 1754 if (Object.keys(shippingArr).length > 0) 1755 data.shippingAddress = shippingArr 1756 1757 if (valid && "email" in data) { 1758 isSubmitting = true 1759 await submitData(data, returnUrl) 1760 isSubmitting = false 1761 } else { 1762 console.warn("invalid data received") 1763 } 1764 } 1765 1766 function toggleContent(content) { 1767 let hrefValue = content.startsWith("#") ? content.slice(1) : content 1768 1769 let showIds = [] 1770 let hideIds = [] 1771 1772 let parts = hrefValue.split(":") 1773
1774 parts.forEach((part) => { 1775 if (part.startsWith("show--")) { 1776 showIds = part.replace("show--", "").split(",") 1777 } else if (part.startsWith("hide--")) { 1778 hideIds = part.replace("hide--", "").split(",") 1779 } 1780 }) 1781 1782 if (showIds.length) { 1783 $.each(showIds, function () { 1784 $(`#${this}`).show() 1785 }) 1786 } 1787 1788 if (hideIds.length) { 1789 $.each(hideIds, function () { 1790 $(`#${this}`).hide() 1791 }) 1792 } 1793 } 1794 1795 function scrollTo(href) { 1796 const scrollToId = href.split("-").pop() 1797 1798 if (!scrollToId) { 1799 throw new Error("Unable to extract ID from the href.") 1800 } 1801 1802 const targetElement = $(`#${scrollToId}`) 1803 1804 if (targetElement.length === 0) { 1805 throw new Error(`Element with ID "${scrollToId}" not found.`) 1806 } 1807 1808 $("html, body").animate( 1809 { scrollTop: targetElement.offset().top }, 1810 { duration: 500, easing: "swing" } 1811 ) 1812 } 1813 1814 async function ocuSubmitOrder(href) { 1815 if (isSubmitting) return 1816 1817 // No product setup 1818 if (href == "#") { 1819 redirectAfterSubmit() 1820 return 1821 } 1822 1823 if (!getCookie("paymentId") || !getCookie("customerId")) { 1824 redirectAfterSubmit() 1825 return 1826 } 1827 1828 const contactSession = checkSessionValid("contactSession") 1829 if (!contactSession) return 1830 1831 try { 1832 const contact = await sendRequest( 1833 `contacts/session/${contactSession}` 1834 ) 1835 1836 const payload = { 1837 url: window.location.href, 1838 contactId: contact._id, 1839 priceId: href, 1840 ocuSubmit: true 1841 } 1842 1843 const paymentResponse = await sendRequest("user/payment", { 1844 method: "POST", 1845 headers: { "Content-Type": "application/json" }, 1846 body: JSON.stringify(payload) 1847 }) 1848 1849 if (paymentResponse.requiresAction) { 1850 await ensureStripe() 1851 const stripe = new window.Stripe(window.public_stripe_key, { 1852 stripeAccount: paymentResponse.accountId 1853 }) 1854 const { error: confirmError } = 1855 await stripe.confirmCardPayment( 1856 paymentResponse.clientSecret 1857 ) 1858 if (confirmError) { 1859 await sendRequest("user/cancel-and-decrease", { 1860 method: "POST", 1861 headers: { 1862 "Content-Type": "application/json" 1863 }, 1864 body: JSON.stringify(paymentResponse) 1865 }) 1866 1867 showToast("Payment was not successful", { 1868 type: "error", 1869 hasCloseButton: true 1870 }) 1871 throw new Error(confirmError.message) 1872 } 1873 } 1874 const redirectURL = await isExistRedirectURL() 1875 if (redirectURL) { 1876 window.location.href = redirectURL 1877 } else if (window.redirect_url_after_submit) { 1878 window.location.href = window.redirect_url_after_submit 1879 } else { 1880 try { 1881 await redirectNextPage({ ocuSubmit: true }) 1882 } catch ({ error }) { 1883 // prevent multiple payments 1884 clearCookie("paymentId") 1885 clearCookie("customerId") 1886 } 1887 } 1888 } catch (error) { 1889 console.error(error) 1890 // declined.show() 1891 } finally { 1892 isSubmitting = false 1893 } 1894 } 1895 1896 $(".elSMS").each(function () { 1897 // hide SMS form on load the page 1898 $(this).find(".smsExtras").hide() 1899 $(this) 1900 .find(".smsCheckInput") 1901 .on("change", (evt) => { 1902 if (evt.target.checked) { 1903 $(this).find(".smsExtras").show() 1904 } else { 1905 $(this).find(".smsExtras").hide() 1906 } 1907 }) 1908 1909 $(this) 1910 .find("select") 1911 .on("change", () => { 1912 const val = $(this).find("select").val() 1913 $(this).find("input[name='phone']").val(`+${val} `) 1914 }) 1915 }) 1916 1917 $(".elButton, [data-url]").on("click", async function (evt) { 1918 const button = $(this) 1919 const action = 1920 button.attr("data-action-type") ?? button.attr("data-url") 1921 const target = button.attr("target") ?? button.attr("data-target") 1922 // used to redirect after form submission 1923 // if not exist, redriect to next step in the funnel 1924 const return_url = button.attr("data-return-url") 1925 const href = button.attr("href") 1926 1927 if (action === "#website-url#") return 1928 1929 evt.preventDefault() 1930 evt.stopPropagation() 1931 1932 if (action === "#open-popup") showGlobalPopup() 1933 else if (action === "#close-popup") console.log("close popup") 1934 else if (action === "#submit-form") onSubmitPage(return_url) 1935 else if (action === "#next-url#") redirectNextPage() 1936 else if (action === "#yes-link") { 1937 button.css("opacity", "0.5") 1938 button 1939 .find(".button-main-text") 1940 .append( 1941 '<i class="fa fa-spinner fa-spin" style="margin-left: 4px;"></i>' 1942 ) 1943 1944 await ocuSubmitOrder(href) 1945 1946 button.find(".button-main-text i.fa-spinner").remove() 1947 button.css("opacity", "1") 1948 } else if (action === "#no-link") redirectAfterSubmit() 1949 else if (action === "#toggle-content#") toggleContent(href) 1950 else if (action === "#scroll-to#") scrollTo(href) 1951 else if (action && target === "_blank") 1952 window.open(action, "_blank") 1953 else if (action) window.location.href = action 1954 else console.warn("invalid data-url", action) 1955 }) 1956 1957 $(".elInput:not(.elOrder2StepWrapper .elInput)").on( 1958 "input", 1959 function () { 1960 const isCheckbox = $(this).attr("type") === "checkbox" 1961 const value = isCheckbox 1962 ? $(this).is(":checked") 1963 : $(this).val() 1964 const name = $(this).prop("name") 1965 const required = $(this).prop("required") 1966 1967 if (name && name !== "sms_approved") { 1968 if (isCheckbox) { 1969 $(`.elInput[name="${name}"]`).prop("checked", value) 1970 if (required && isDirty && !value) { 1971 $(`.elInput[name="${name}"]`).css( 1972 "outline", 1973 "1px solid #b91517" 1974 ) 1975 $(`.elInput[name="${name}"]`).css( 1976 "outline-offset", 1977 "-1px" 1978 ) 1979 } else { 1980 $(`.elInput[name="${name}"]`).css("outline", "") 1981 $(`.elInput[name="${name}"]`).css( 1982 "outline-offset", 1983 "" 1984 ) 1985 } 1986 } else { 1987 $(`.elInput[name="${name}"]`).val(value) 1988 if (required && isDirty && !value) { 1989 $(`.elInput[name="${name}"]`).css( 1990 "border-color", 1991 "#b91517" 1992 ) 1993 } else { 1994 $(`.elInput[name="${name}"]`).css( 1995 "border-color", 1996 "" 1997 ) 1998 } 1999 } 2000 } 2001 } 2002 ) 2003 }) 2004 2005 // ------------- 2 step order ----------- 2006 $(function () { 2007 let products = [] 2008 let selectedProductId = "" 2009 const formatCurrency = (value, currency) => { 2010 if (isNaN(+value)) { 2011 return value 2012 } 2013 2014 const formatter = new Intl.NumberFormat("en-US", { 2015 style: "currency", 2016 currency: currency 2017 }) 2018 2019 return formatter.format(value / 100) 2020 } 2021 2022 $(".elCreditCardForm") 2023 .find(".elInput") 2024 .on("input", function (e) { 2025 const name = $(this).prop("name") 2026 let value = $(this).val() 2027 value = value.replace(/\D/g, "") 2028 2029 if (name === "cc_number") { 2030 value = value.substring(0, 16) 2031 $(this).val(value.replace(/(\d{4})(?=\d)/g, "$1 ")) 2032 } else if (name === "cc_expiry") { 2033 if (value.length === 1 && parseInt(value) > 1) { 2034 value = "0" + value + "/" 2035 } else if (value.length === 2) {
2036 const month = parseInt(value) 2037 if (month > 12 || month === 0) { 2038 value = "12/" 2039 } else { 2040 value += "/" 2041 } 2042 } else if (value.length > 2) { 2043 const currentYear = new Date() 2044 .getFullYear() 2045 .toString() 2046 .slice(-2) 2047 if (value.length > 4) { 2048 value = value.substring(0, 4) 2049 } 2050 const monthPart = value.substring(0, 2) 2051 let yearPart = value.substring(2) 2052 if (yearPart.length === 2) { 2053 if (parseInt(yearPart) < parseInt(currentYear)) { 2054 yearPart = currentYear 2055 } 2056 value = monthPart + "/" + yearPart 2057 } else if (yearPart.length === 1) { 2058 if ( 2059 parseInt(yearPart) < 2060 parseInt(currentYear.substring(0, 1)) 2061 ) { 2062 yearPart = currentYear.substring(0, 1) 2063 } 2064 value = monthPart + "/" + yearPart 2065 } 2066 } 2067 if ( 2068 e.originalEvent && 2069 e.originalEvent.inputType === "deleteContentBackward" && 2070 value.includes("/") 2071 ) { 2072 value = value.substring(0, 2) 2073 } 2074 $(this).val(value) 2075 } else if (name === "cc_cvc") { 2076 if (value.length > 4) { 2077 value = value.substring(0, 4) 2078 } 2079 $(this).val(value) 2080 } 2081 }) 2082 2083 $(".elOrderSummaryWrapper").each(function () { 2084 $(this).find(".elOrderSummaryDetail").empty() 2085 $(this).find(".elProductLoadingIndicator").show() 2086 }) 2087 2088 $(".elProductSelectWrapper").each(function () { 2089 $(this).find(".elProductRadioList").empty() 2090 $(this).find(".elProductDropdownWrapper select").empty() 2091 $(this).find(".elProductLoadingIndicator").show() 2092 }) 2093 2094 if ($(".elProductSelectWrapper").length > 0) { 2095 const showNotFoundMessage = () => { 2096 $(".elProductSelectWrapper").each(function () { 2097 $(this) 2098 .find(".elProductLoadingIndicator") 2099 .empty() 2100 .append("<p>Products not found</p>") 2101 .css({ 2102 "font-size": "14px", 2103 "margin-top": "20px" 2104 }) 2105 2106 $(".elOrder2StepWrapper") 2107 .find(".elOrderCompleteButton") 2108 .prop("disabled", true) 2109 .css({ cursor: "not-allowed" }) 2110 }) 2111 $(".elOrderSummaryWrapper").remove() 2112 } 2113 2114 const appendProducts = () => { 2115 const options = products.map((p) => { 2116 const _value = formatCurrency(p.unit_amount, p.currency) 2117 const interval = p?.recurring?.interval 2118 ? ` / ${p?.recurring?.interval}` 2119 : "" 2120 const valueStr = `${_value}${interval}` 2121 return { 2122 id: p.id, 2123 name: p?.overriddenName ?? p.product.name, 2124 amount: p?.overriddenPrice ?? valueStr, 2125 images: p.product.images 2126 } 2127 }) 2128 2129 selectedProductId = options[0]?.id 2130 2131 const radioOptions = options 2132 .map( 2133 (p, i) => ` 2134 <label class="elProductRadioItem"> 2135 <div class="elProductName"> 2136 <input type="radio" 2137 name="product" 2138 value="${p.id}" 2139 ${i === 0 ? "checked" : ""} 2140 ></input>
2141 <span>${p.name}</span> 2142 </div> 2143 <div class="elProductPrice">${p.amount}</div> 2144 </label> 2145 ` 2146 ) 2147 .join("") 2148 const dropdownOptions = options 2149 .map( 2150 (p, i) => ` 2151 <option value="${p.id}" ${i === 0 ? "selected" : ""}> 2152 ${p.name} - ${p.amount} 2153 </option> 2154 ` 2155 ) 2156 .join("") 2157 2158 const updateOrderSummary = (productId) => { 2159 const op = options.find((p) => p.id === productId) 2160 if (op && productId) { 2161 $(".elOrderSummaryWrapper").each(function () { 2162 $(this) 2163 .find(".elOrderSummaryDetail") 2164 .html( 2165 `<span>${op.name}</span><spanclass="elProductPrice">${op.amount}</span>` 2166 ) 2167 }) 2168 } 2169 } 2170 $(".elProductSelectWrapper").each(function () { 2171 $(this).find(".elProductLoadingIndicator").remove() 2172 $(this) 2173 .find(".elProductRadioList") 2174 .append(radioOptions) 2175 .find("input") 2176 .on("input", function (evt) { 2177 selectedProductId = evt.target.value 2178 updateOrderSummary(selectedProductId) 2179 }) 2180 $(this) 2181 .find("select") 2182 .append(dropdownOptions) 2183 .on("input", function (evt) { 2184 selectedProductId = evt.target.value 2185 updateOrderSummary(selectedProductId) 2186 }) 2187 }) 2188 updateOrderSummary(selectedProductId) 2189 2190 $(".elOrderSummaryWrapper").each(function () { 2191 $(this).find(".elProductLoadingIndicator").remove() 2192 }) 2193 } 2194 2195 $(async () => { 2196 try { 2197 const url = encodeURIComponent(window.location.href) 2198 const { result } = await sendRequest( 2199 `pages/products?url=${url}` 2200 ) 2201 if (!result.length) { 2202 showNotFoundMessage() 2203 } else { 2204 products = [...result] 2205 appendProducts() 2206 } 2207 } catch (error) { 2208 console.log(error) 2209 showNotFoundMessage() 2210 } 2211 }) 2212 } 2213 2214 $(".elInputFieldWrapper.hide").remove() 2215 2216 $(".elOrder2StepWrapper").each(function () { 2217 let isDirty = false 2218 let hasError = false 2219 let isSubmitting = false 2220 let data = null 2221 let stripe 2222 let cardElement 2223 2224 const stripeFieldLayout = $(".elCreditCardForm").attr( 2225 "data-stripe-field-layout" 2226 ) 2227 const isOneField = stripeFieldLayout === "oneField" 2228 2229 // TODO: remove me once we enable shippind detail form 2230 $(".elShippingWrapper.hide").remove() 2231 2232 const goToNext = $(this).find(".elOrder2Step1 .elOrder2StepButton") 2233 const backToStep1 = $(this).find(".elOrder2GoBackStep") 2234 const completeOrder = $(this).find(".elOrderCompleteButton") 2235 const declined = $(this) 2236 .find(".elCreditCardForm .orderDeclinedMessage") 2237 .hide() 2238 2239 // Initialize Stripe after button elements are defined 2240 if (isOneField) { 2241 ensureStripe() 2242 .then(() => { 2243 stripe = new window.Stripe(window.public_stripe_key) 2244 const elements = stripe.elements() 2245 cardElement = elements.create("card", { 2246 hidePostalCode: true, 2247 disableLink: true 2248 }) 2249 2250 let isMounted = false 2251 cardElement.on("ready", () => { 2252 isMounted = true 2253 }) 2254 2255 // Only mount the card element when step 2 is shown 2256 goToNext.on("click", () => { 2257 const mountElement = 2258 document.querySelector(".stripeCardElement") 2259 if (mountElement && !isMounted) { 2260 cardElement.mount(".stripeCardElement") 2261 } 2262 }) 2263 2264 // Unmount when going back to step 1 2265 backToStep1.on("click", () => { 2266 if (isMounted) { 2267 cardElement.unmount() 2268 isMounted = false 2269 } 2270 }) 2271 2272 // Add card validation listener 2273 cardElement.on("change", (event) => { 2274 if (event.error) { 2275 declined.text(event.error.message).show() 2276 } else { 2277 declined.hide() 2278 } 2279 }) 2280 }) 2281 .catch((error) => { 2282 console.error(error) 2283 declined 2284 .text( 2285 "Payment form failed to load. Please refresh the page." 2286 ) 2287 .show() 2288 }) 2289 } 2290 2291 function addError(input) { 2292 hasError = true 2293 $(input).css("border-color", "#b91517") 2294 } 2295 2296 function removeError(input) { 2297 $(input).css("border-color", "") 2298 } 2299 2300 function validateInput() { 2301 const name = $(this).prop("name") 2302 const value = $(this).val() 2303 const required = $(this).prop("required") 2304 2305 if (name === "name") { 2306 if (value.length < 3) { 2307 addError(this) 2308 } else { 2309 removeError(this) 2310 } 2311 } else if (name === "email") { 2312 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) { 2313 addError(this) 2314 } else { 2315 removeError(this) 2316 } 2317 } else if (name === "phone") { 2318 try { 2319 const parsedPhone = new libphonenumber.parsePhoneNumber( 2320 value, 2321 data?.country_codes_phone || "US" 2322 ) 2323 if (!parsedPhone.isPossible()) { 2324 addError(this) 2325 } else { 2326 removeError(this) 2327 } 2328 } catch (error) { 2329 console.error("Error validating phone number:", error) 2330 addError(this) 2331 } 2332 } else if ( 2333 name === "shipping_address" || 2334 name === "shipping_city" || 2335 name === "shipping_state" || 2336 name === "shipping_zip" || 2337 name === "shipping_country" 2338 ) { 2339 if (required && !value) { 2340 addError(this) 2341 } else { 2342 removeError(this) 2343 } 2344 } else if (name === "cc_number") { 2345 const len = value.replace(/\s+/g, "").length 2346 if (!(len >= 15 && len < 20)) { 2347 addError(this) 2348 } else { 2349 removeError(this) 2350 } 2351 } else if (name === "cc_expiry") { 2352 if (!/^(0[1-9]|1[0-2])\/?([0-9]{2})$/.test(value)) { 2353 addError(this) 2354 } else { 2355 const date = new Date()
2356 const month = parseInt(value.substring(0, 2), 10) 2357 const year = parseInt(value.substring(3), 10) + 2000 2358 2359 const y = date.getFullYear() 2360 const m = date.getMonth() 2361 if (!(year > y || (year === y && month >= m))) { 2362 addError(this) 2363 } else { 2364 removeError(this) 2365 } 2366 } 2367 } else if (name === "cc_cvc") { 2368 if (/^\d{3,4}$/.test(value)) { 2369 removeError(this) 2370 } else { 2371 addError(this) 2372 } 2373 } 2374 2375 if (!data) data = {} 2376 data[name] = value 2377 } 2378 2379 const submitOrder = async () => { 2380 try { 2381 isSubmitting = true 2382 2383 const product = products.find( 2384 (p) => p.id === selectedProductId 2385 ) 2386 2387 const { _id: userContactId, sessionId: contactSessionId } = 2388 await sendRequest("contacts", { 2389 method: "POST", 2390 headers: { 2391 "Content-Type": "application/json" 2392 }, 2393 body: JSON.stringify({ 2394 email: data.email, 2395 name: data.name, 2396 url: window.location.href 2397 }) 2398 }) 2399 2400 saveSessionId("contactSession", contactSessionId) 2401 2402 let paymentMethod = null 2403 2404 if (isOneField) { 2405 // Validate card is mounted 2406 if (!cardElement) { 2407 throw new Error( 2408 "Stripe card element not initialized" 2409 ) 2410 } 2411 2412 // Check if card details are complete 2413 if (!cardElement._complete) { 2414 declined 2415 .text("Please enter complete card details") 2416 .show() 2417 throw new Error("Incomplete card details") 2418 } 2419 2420 const result = await stripe.createPaymentMethod({ 2421 type: "card", 2422 card: cardElement 2423 }) 2424 2425 if (!result || !result.paymentMethod) { 2426 declined 2427 .text( 2428 "Failed to create payment method. Please check your card details." 2429 ) 2430 .show() 2431 throw new Error("Failed to create payment method") 2432 } 2433 2434 const accountPaymentMethod = await sendRequest( 2435 "user/create-payment-method", 2436 { 2437 method: "POST", 2438 headers: { 2439 "Content-Type": "application/json" 2440 }, 2441 body: JSON.stringify({ 2442 paymentMethodId: result.paymentMethod.id, 2443 accountId: product.accountId, 2444 name: data.name, 2445 email: data.email 2446 }) 2447 } 2448 ) 2449 2450 paymentMethod = accountPaymentMethod?.paymentMethod 2451 } else { 2452 const exp_month = 2453 data.cc_expiry[0] === "0" 2454 ? data.cc_expiry.substring(1, 2) 2455 : data.cc_expiry.split("/")[0] 2456 const exp_year = data.cc_expiry.split("/")[1] 2457 2458 const response = await fetch( 2459 "https://api.stripe.com/v1/payment_methods", 2460 { 2461 method: "POST", 2462 headers: { 2463 "Content-Type": 2464 "application/x-www-form-urlencoded", 2465 "Stripe-Account": product.accountId, 2466 Authorization: `Bearer ${window.public_stripe_key}` 2467 }, 2468 body: new URLSearchParams({ 2469 type: "card",
2470 "card[number]": data.cc_number, 2471 "card[exp_month]": `${exp_month}`, 2472 "card[exp_year]": `20${exp_year}`, 2473 "card[cvc]": data.cc_cvc, 2474 "metadata[name]": data.name, 2475 "metadata[email]": data.email 2476 }) 2477 } 2478 ) 2479 if (!response.ok) { 2480 throw new Error("failed connecting stripe!") 2481 } 2482 paymentMethod = await response.json() 2483 } 2484 2485 if (paymentMethod) { 2486 const payload = { 2487 url: window.location.href, 2488 contactId: userContactId, 2489 priceId: product.id, 2490 paymentId: paymentMethod.id, 2491 accountId: product.accountId, 2492 type: product.type, 2493 productName: product.product.name, 2494 productFreeTrialDays: product?.freeTrialDays ?? 0 2495 } 2496 const paymentResponse = await sendRequest( 2497 "user/payment", 2498 { 2499 method: "POST", 2500 headers: { "Content-Type": "application/json" }, 2501 body: JSON.stringify(payload) 2502 } 2503 ) 2504 2505 await ensureStripe() 2506 stripe = new window.Stripe(window.public_stripe_key, { 2507 stripeAccount: product.accountId 2508 }) 2509 2510 if (paymentResponse.requiresAction) { 2511 const { error: confirmError } = 2512 await stripe.confirmCardPayment( 2513 paymentResponse.clientSecret 2514 ) 2515 if (confirmError) { 2516 await sendRequest("user/cancel-and-decrease", { 2517 method: "POST", 2518 headers: { 2519 "Content-Type": "application/json" 2520 }, 2521 body: JSON.stringify({ 2522 ...paymentResponse, 2523 accountId: product.accountId 2524 }) 2525 }) 2526 2527 showToast("Payment was not successful", { 2528 type: "error", 2529 hasCloseButton: true 2530 }) 2531 throw new Error(confirmError.message) 2532 } 2533 showToast("Payment was successful", { 2534 hasCloseButton: true 2535 }) 2536 } 2537 2538 const redirectURL = await isExistRedirectURL() 2539 if (redirectURL) { 2540 window.location.href = redirectURL 2541 } 2542 2543 const exteral = $(this).attr("data-external-url") 2544 if (exteral) { 2545 window.location.href = exteral 2546 } else { 2547 try { 2548 await redirectNextPage() 2549 } catch ({ error }) { 2550 showToast("Payment was successful", { 2551 hasCloseButton: true 2552 }) 2553 console.error(error) 2554 } 2555 } 2556 } 2557 } catch (error) { 2558 console.error(error) 2559 await sendRequest("contacts", { 2560 method: "POST", 2561 headers: { 2562 "Content-Type": "application/json" 2563 }, 2564 body: JSON.stringify({ 2565 email: data.email, 2566 name: data.name, 2567 url: window.location.href, 2568 requestFrom: "purchase_form_step_1" 2569 }) 2570 }) 2571 declined.show() 2572 } finally { 2573 isSubmitting = false 2574 } 2575 } 2576 2577 $(this) 2578 .find(".elInput") 2579 .on("input", function () { 2580 if (isDirty) validateInput.bind(this)() 2581 }) 2582 2583 // next button! 2584 goToNext.on("click", async () => { 2585 isDirty = true 2586 hasError = false 2587 2588 $(this).find(".elOrder2Step1 .elInput").each(validateInput) 2589 2590 if (!hasError) { 2591 isDirty = false 2592 2593 // First send the contact request 2594 const data = { 2595 url: window.location.href, 2596 requestFrom: "purchase_form_step_1" 2597 } 2598 2599 $(this) 2600 .find(".elOrder2Step1 .elInput") 2601 .each(function () { 2602 const inputName = $(this).attr("name") 2603 const inputValue = $(this).val() 2604 2605 inputName === "phone" 2606 ? (data.phoneNumber = inputValue) 2607 : (data[inputName] = inputValue) 2608 }) 2609 2610 sendRequest("contactsNew", { 2611 method: "POST", 2612 headers: { "Content-Type": "application/json" }, 2613 body: JSON.stringify(data) 2614 }).then(() => { 2615 console.log("contactsNew") 2616 }) 2617 2618 // Then update classes to show step 2 2619 $(this).removeClass("elOrderActiveStep1") 2620 $(this).addClass("elOrderActiveStep2") 2621 } 2622 }) 2623 2624 // back to shipping details form 2625 backToStep1.on("click", () => { 2626 isDirty = false 2627 $(this).removeClass("elOrderActiveStep2") 2628 $(this).addClass("elOrderActiveStep1") 2629 }) 2630 2631 // submit orders 2632 completeOrder.on("click", async () => { 2633 isDirty = true 2634 hasError = false 2635 declined.hide() 2636 completeOrder.prop("disabled", true) 2637 completeOrder.children().hide() 2638 completeOrder.append('<i class="fa fa-spinner fa-spin"></i>') 2639 2640 if (!isOneField) { 2641 $(this).find(".elOrder2Step2 .elInput").each(validateInput) 2642 } 2643 2644 if (!isSubmitting && selectedProductId && !hasError) { 2645 await submitOrder() 2646 } 2647 2648 completeOrder.find("i.fa-spinner").remove() 2649 completeOrder.children().show() 2650 completeOrder.prop("disabled", false) 2651 }) 2652 }) 2653 }) 2654}
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.