var observer = new IntersectionObserver( function (entries) { // no intersection with screen if (entries[0].intersectionRatio === 0) document .querySelector(".mcs-pricing--header") .classList.add("mcs-pricing--header-sticky"); // fully intersects with screen else if (entries[0].intersectionRatio === 1) document .querySelector(".mcs-pricing--header") .classList.remove("mcs-pricing--header-sticky"); }, { threshold: [0, 1] } ); observer.observe(document.querySelector(".mcs-pricing--target")); // funcs and variables to render respective prices ------- CICS-18019 // All Price Cards Prices const priceElements = qsa(".mcs-tier--mcs-pricing-price"); // All AddOns Prices const addOnEls = qsa(".mcs-link--copy-add-on"); // per month elemnts const perMonthElements = qsa(".mcs-tier--mcs-pricing-term"); // All Feature List Cells Prices const tableCellPriceEls = qsa(".mcs-table--cell-price"); // loading spinners for All Price Cards const loaders = qsa(".mcs-tier--pricing-loader"); // hide 'per month' text perMonthElements.forEach(({ style }) => (style.opacity = "0")); // hide prices elements priceElements.forEach(({ style }) => (style.opacity = "0")); // hide add-ons addOnEls.forEach(({ style }) => (style.opacity = "0")); const FALLBACK_PRICES = [0, 15, 49, 249]; const ADD_ONS_TEXT_MAP = { 240950: "per 10 GB monthly", 240951: "per 50 GB monthly", 240952: "per 500 GB monthly", }; const FALLBACK_ADD_ON_PRICES = [ { id: "240951", price: 1.5 }, { id: "240952", price: 2 }, { id: "240950", price: 3.5 }, ]; const addOnTooltips = FALLBACK_ADD_ON_PRICES.map(({ id }) => document.getElementById(`tooltip-${id}`) ); // swap out const ENV value for production/dev to accommodate manual testing, new features, etc. const ENV = "https://app.cimediacloud.com"; const IP_API = "https://api.ipify.org/?format=json"; // https://www.ipify.org/ const PRICES_API = `${ENV}/api/product-prices`; const GEO_API = `${ENV}/api/location-lookup`; function qsa(selector, parent = document) { return [...parent.querySelectorAll(selector)]; } // index is used to determine if price is for Pro, Team, or Business function formatPrice(price, currency, index = null, includeCents = false) { const formattedPrice = price.toLocaleString(undefined, { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: 2, }); let result = includeCents ? formattedPrice : formattedPrice.replace(/\.00$/, ""); // ensure index is not 0 if (currency === "JPY" && index) result += " (税込)"; return result; } async function getUserIP() { try { const res = await fetch(IP_API); const data = await res.json(); const { ip } = data; return ip; } catch (error) { throw new Error("Failed to fetch user IP: ", error); } } async function fetchGeoData(ip) { try { const res = await fetch(`${GEO_API}?ip=${ip}`); const data = await res.json(); return data; } catch (error) { throw new Error("Failed to fetch geolocation data"); } } async function fetchPrices(currencyId) { try { const res = await fetch(`${PRICES_API}?currencyId=${currencyId}`); const pricingData = await res.json(); return pricingData; } catch (error) { throw new Error("Failed to fetch prices: ", error); } } async function renderPrices() { // exclude add-on products from cleverbridge response const addOnIds = ["240950", "240951", "240952"]; try { const userIp = await getUserIP(); const geolocationData = await fetchGeoData(userIp); const currencySymbol = geolocationData ? geolocationData.currencySymbol : "USD"; // fallback to USD const pricingData = await fetchPrices(currencySymbol); const isJPY = currencySymbol === "JPY"; // excludes add-on products const filteredPlans = pricingData.products.filter( ({ id }) => !addOnIds.includes(id) ); // includes only add-on products const filteredAddOns = pricingData.products.filter(({ id }) => addOnIds.includes(id) ); filteredPlans.forEach(({ prices }, index) => { const priceText = prices .map(({ currencyId, unitPrice }) => formatPrice(unitPrice, currencyId, index + 1) ) .join(""); // Cards/Cells for Pro, Team, Business are indices 1, 2, 3 if (priceElements[index + 1] && tableCellPriceEls[index + 1]) { // Set Pro, Team, and Business Card prices priceElements[index + 1].textContent = priceText; // Set Pro, Team, and Business Cells in Feature List prices tableCellPriceEls[index + 1].textContent = `${priceText} per month`; } }); filteredAddOns.forEach(({ id, prices }) => { const priceText = prices .map(({ currencyId, unitPrice }) => formatPrice(unitPrice, currencyId, true, true) ) .join(""); const addOnElement = document.getElementById(id); // Set Add-on price if (addOnElement) { addOnElement.textContent = `${priceText} ${ADD_ONS_TEXT_MAP[id]}`; } const tooltipIndex = addOnTooltips.findIndex( (tooltip) => tooltip.id === `tooltip-${id}` ); if (tooltipIndex !== -1) { addOnTooltips[tooltipIndex].textContent = `${priceText}`; } }); const firstProductData = pricingData.products[0].prices[0]; // Card/Cell for Free is index 0 let freeCard = priceElements[0]; let freeTableCell = tableCellPriceEls[0]; if (freeCard && freeTableCell && firstProductData) { const { currencyId } = firstProductData; const formattedPrice = formatPrice(0, currencyId, 0); // Set Free Card price freeCard.textContent = formattedPrice; // Set Free Cell in Feature List price freeTableCell.textContent = `${formattedPrice} per month`; } if (isJPY) { // reduce font size for JPY prices priceElements.forEach(({ style }) => { style.fontSize = "32px"; }); } } catch (error) { // Set fallback to USD prices in All Cards priceElements.forEach((element, index) => { element.textContent = formatPrice(FALLBACK_PRICES[index], "USD"); }); // Set fallback to USD prices in All Feature List Cells tableCellPriceEls.forEach((element, index) => { element.textContent = FALLBACK_PRICES[index] !== undefined ? `${formatPrice(FALLBACK_PRICES[index], "USD")} per month` : "Contact for pricing"; }); // Set fallback to USD prices in All Add Ons addOnEls.forEach((element, index) => { element.textContent = `${formatPrice( FALLBACK_ADD_ON_PRICES[index].price, "USD", true, true )} ${ADD_ONS_TEXT_MAP[FALLBACK_ADD_ON_PRICES[index].id]}`; }); addOnTooltips.forEach((element, index) => { element.textContent = `${formatPrice( FALLBACK_ADD_ON_PRICES[index].price, "USD", true, true )}`; }); console.error("Error Occurred:", error); } finally { // remove loading spinners loaders.forEach(({ style }) => (style.display = "none")); // render prices text priceElements.forEach(({ style }) => { style.opacity = "1"; }); // render 'per month' text perMonthElements.forEach(({ style }) => { style.display = "block"; style.opacity = "1"; }); // render add-ons addOnEls.forEach(({ style }) => { style.opacity = "1"; }); } } // wait for resources to load, then render prices // can also call renderPrices() directly if needed -> renderPrices() window.addEventListener("load", renderPrices); // ------------- Baresquare custom events ---------------- // ------- Declare custom event names here ------- const BUY_NOW_CLICK_EVENT = "mcs_buy_now_click"; // ------- Declare custom event detail objects here ------- function buyNowEventDetail(planName) { return { buy_plan: `Buy ${planName}`, }; } // ------- Helper functions for custom events ------- function createCustomEvent(eventName, detail) { return new CustomEvent(eventName, { detail, }); } function createDispatcher(eventName, eventDetail) { const event = createCustomEvent(eventName, eventDetail); document.dispatchEvent(event); } function addClickListener(className, eventName, eventDetail) { const elements = qsa(className); elements.forEach((element) => { element.addEventListener("click", () => { createDispatcher(eventName, eventDetail); }); }); } // ------- Add click listeners to CSS class names to dispatch custom events ------- addClickListener(".click-free", BUY_NOW_CLICK_EVENT, buyNowEventDetail("Free")); addClickListener(".click-pro", BUY_NOW_CLICK_EVENT, buyNowEventDetail("Pro")); addClickListener(".click-team", BUY_NOW_CLICK_EVENT, buyNowEventDetail("Team")); addClickListener( ".click-business", BUY_NOW_CLICK_EVENT, buyNowEventDetail("Business") ); // ------- logging custom events ------- document.addEventListener(BUY_NOW_CLICK_EVENT, (e) => console.log(e.detail.buy_plan) );