'use strict'; (function () { // Configuration var CONFIG = { IFRAME_URL: 'https://go.nowcircular.com/widget', CIRCULAR_LOGO: 'https://assets.nowcircular.sg/images/circular-logo.png', API_BASE_URL: 'https://nowcircular.sg/api/v1', DEBOUNCE_DELAY: 200, // Debounce delay in milliseconds }; // Primary DOM Elements var widgetContainer = document.getElementById('circular-on-site-widget'); // State var banner = null; var modal = null; var observer = null; // Main execution function init() { initStyles(); modal = createModal(); fetchPricing(initWidget, handleError); var keyEvents = bindKeyEvents(); initAttributeObserver(); // Cleanup window.addEventListener('unload', cleanup(keyEvents)); } function cleanup(keyEvents) { return function () { if (banner) { banner.unload(); } if (modal) { modal.unload(); } if (keyEvents) { keyEvents.unload(); } if (observer) { observer.disconnect(); } }; } // Widget initialization function initWidget(productPricingData) { if (widgetContainer) { if (banner) { banner.update(productPricingData); } else { banner = createBanner(productPricingData); widgetContainer.innerHTML = ''; // Clear any error messages widgetContainer.appendChild(banner.element); } } } function handleError() { console.warn('Circular Widget: Failed to initialize widget.'); if (widgetContainer) { widgetContainer.innerHTML = ''; // Clear the widget container banner = null; // Reset the banner state } } // Banner creation and management function createBanner(productPricingData) { var bannerElement = document.createElement('div'); bannerElement.className = 'circular-banner'; function render(data) { var monthlyPrice = data.monthlyPrice; var durationMonths = data.durationMonths; const content = `

Subscribe from $${monthlyPrice}/month for ${durationMonths} months

Select Circular at checkout Learn more
Powered by
`; bannerElement.innerHTML = content; } function update(newProductPricingData) { render({ monthlyPrice: formatPrice(newProductPricingData.monthly_price_cents / 100), durationMonths: newProductPricingData.duration_months || '24', }); } update(productPricingData); bannerElement.addEventListener('click', modal.openModal); return { element: bannerElement, update: update, unload: function () { bannerElement.removeEventListener('click', modal.openModal); }, }; } // Data fetching function fetchPricing(success, error) { var attributes = getWidgetAttributes(); var sku = attributes.sku; var partner = attributes.partner; var fullPurchasePrice = attributes.fullPurchasePrice; if (!sku || !partner || !fullPurchasePrice) { console.warn('Circular Widget: Missing data attributes.'); error(); return; } xhrGet({ url: getPricingUrl(partner, sku), success: success, error: function () { console.warn('Circular Widget: No SKU mapping found for partner: ' + partner + ' and SKU: ' + sku); error(); }, }); } function getWidgetAttributes() { return { sku: widgetContainer ? widgetContainer.getAttribute('data-sku') : null, partner: widgetContainer ? widgetContainer.getAttribute('data-partner') : null, fullPurchasePrice: widgetContainer ? widgetContainer.getAttribute('data-full-purchase-price') : null, }; } // Attribute change observer function initAttributeObserver() { if (!widgetContainer) return; var debouncedFetchPricing = debounce(function () { fetchPricing(initWidget, handleError); }, CONFIG.DEBOUNCE_DELAY); observer = new MutationObserver(function (mutations) { var hasRelevantChanges = mutations.some(function (mutation) { return ( mutation.type === 'attributes' && ['data-sku', 'data-partner', 'data-full-purchase-price'].indexOf(mutation.attributeName) !== -1 ); }); if (hasRelevantChanges) { debouncedFetchPricing(); } }); var config = {attributes: true, attributeFilter: ['data-sku', 'data-partner', 'data-full-purchase-price']}; observer.observe(widgetContainer, config); } // Modal functions function createModal() { var modalElement = document.createElement('div'); modalElement.id = 'circular-on-site-modal'; var spinner = document.createElement('div'); spinner.className = 'circular-spinner'; var iframe = document.createElement('iframe'); modalElement.appendChild(spinner); modalElement.appendChild(iframe); document.body.appendChild(modalElement); function openModal() { iframe.style.display = 'none'; spinner.style.display = 'block'; iframe.src = CONFIG.IFRAME_URL; modalElement.classList.add('active'); } function closeModal() { modalElement.classList.remove('active'); iframe.removeAttribute('src'); spinner.style.display = 'block'; } function iframeLoad() { spinner.style.display = 'none'; iframe.style.display = 'block'; } iframe.addEventListener('load', iframeLoad); modalElement.addEventListener('click', closeModal); return { openModal: openModal, closeModal: closeModal, unload: function () { iframe.removeEventListener('load', iframeLoad); modalElement.removeEventListener('click', closeModal); }, }; } // Event binding function bindKeyEvents() { function handleEscapeKey(event) { if (event.key === 'Escape') { modal.closeModal(); } } document.addEventListener('keydown', handleEscapeKey); return { unload: function () { document.removeEventListener('keydown', handleEscapeKey); }, }; } // Utility functions function initStyles() { var style = document.createElement('style'); style.innerHTML = ` @import url('https://fonts.googleapis.com/css2?family=Lexend+Deca:wght@400;500&display=swap'); #circular-on-site-widget { font-family: 'Lexend Deca', sans-serif; } #circular-on-site-widget .circular-banner { padding: 12px 16px; display: flex; flex-direction: row; flex-wrap: wrap; justify-content: space-between; align-items: flex-start; gap: 8px; border-radius: 0px 5px 5px 0px; border-left: 2px solid #EA2E5D; background: #F6F4FC; } #circular-on-site-widget .subscription-content { display: flex; flex-direction: column; justify-content: center; align-items: flex-start; gap: 8px; flex-grow: 1; } #circular-on-site-widget .circular-banner h2 { color: #190644; font-size: 14px; font-weight: 500; line-height: 16px; margin: 0 0 4px 0; } #circular-on-site-widget .subscription-details { display: flex; gap: 4px; } #circular-on-site-widget .subscription-details span { color: #5E537C; font-size: 12px; font-weight: 400; line-height: 15px; } #circular-on-site-widget .subscription-details a { color: #EA2F5D; font-size: 12px; font-weight: 400; line-height: 15px; text-decoration: underline; } #circular-on-site-widget .powered-by { display: flex; align-items: center; gap: 4px; color: #5E537C; font-size: 9px; font-weight: 500; line-height: 15px; letter-spacing: -0.27px; align-self: flex-start; flex-shrink: 0; order: 1; } #circular-on-site-widget .circular-logo { width: 50px; height: auto; } #circular-on-site-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; visibility: hidden; z-index: 100; } #circular-on-site-modal iframe { width: 80%; max-width: 610px; height: 80%; border: none; } #circular-on-site-modal.active { visibility: visible; } #circular-on-site-modal .circular-spinner { border: 4px solid rgba(255, 255, 255, 0.3); border-radius: 50%; border-top: 4px solid #ffffff; width: 40px; height: 40px; animation: circular-spin 2s linear infinite; } @keyframes circular-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } function xhrGet(params) { var request = new XMLHttpRequest(); request.open('GET', params.url, true); request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); request.onreadystatechange = function () { if (request.readyState === 4) { if (request.status >= 200 && request.status < 300) { params.success(parse(request)); } else if (params.error) { params.error(parse(request)); } } }; request.send(); } function parse(req) { try { return JSON.parse(req.responseText); } catch (e) { return req.responseText; } } function getPricingUrl(partner, sku) { return CONFIG.API_BASE_URL + '/partners/' + partner + '/sku/' + sku + '/lowest-price'; } function formatPrice(value) { if (value === undefined) return ''; return Number(value).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0, }); } function debounce(func, wait) { var timeout; return function () { var context = this; var args = arguments; var later = function () { timeout = null; func.apply(context, args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } init(); })();