/** * @param {HTMLElement} el - product container element * @param {ProductVariant} variant - the product variant that's selected * * You can use the provided Shopyflow objects to implement custom logic. */ console.log('RUNNING LOKI PRODUCT.JS - v8'); /** * This script integrates with Shopyflow to dynamically update product prices, * calculate per-unit pricing, and manage UI visibility. * v8: Fixes a bug where the recurring price would disappear on "Buy Once" selection. */ window.addEventListener('ShopyflowReady', event => { // --- STATE MANAGEMENT --- // Store the current variant and its container element for global access by event handlers. let currentVariant = null; let currentEl = null; // NEW: Store the container element // --- ELEMENT SELECTION --- const recurringPurchaseButton = document.querySelector( '[sf-purchase-option-value="recurring"]' ); const singlePurchaseButton = document.querySelector( '[sf-purchase-option-value="single"]' ); const sellingPlanContainer = document.querySelector( '[sf-change-selling-plan]' ); const buyOncePriceElement = document.querySelector( '[product="buyonce-price"]' ); const recurringPriceElement = document.querySelector( '[product="recurring-price"]' ); const buyOnceAmountElement = document.querySelector( '[product="buyonce-amount"]' ); const recurringAmountElement = document.querySelector( '[product="recurring-amount"]' ); const sellingPlanButtons = document.querySelectorAll( '[sf-selling-plan-value]' ); // --- REUSABLE CORE FUNCTION --- /** * Updates all price and amount elements based on a given variant and an optional selling plan ID. * @param {object} variant - The currently selected product variant object. * @param {string|null} targetPlanId - The ID of the selling plan to display prices for. */ function updatePriceDisplay(variant, targetPlanId) { if (!variant) return; let numberOfCans = 0; if (variant.selectedOptions?.[0]?.value) { const parsedCans = parseInt(variant.selectedOptions[0].value, 10); if (!isNaN(parsedCans) && parsedCans > 0) numberOfCans = parsedCans; } let targetSellingPlanAllocation = null; if (targetPlanId && Array.isArray(variant.sellingPlanAllocations)) { targetSellingPlanAllocation = variant.sellingPlanAllocations.find(alloc => alloc.sellingPlan?.id.endsWith(`/${targetPlanId}`) ); } // Always update the "Buy Once" price based on the variant's base price or the plan's compareAtPrice. const oneTimePrice = targetSellingPlanAllocation ? targetSellingPlanAllocation.priceAdjustments[0].compareAtPrice.amount : variant.price.amount; if (buyOncePriceElement) buyOncePriceElement.innerText = `$${oneTimePrice}`; if (buyOnceAmountElement && numberOfCans > 0) { const pricePerCan = parseFloat(oneTimePrice) / numberOfCans; buyOnceAmountElement.innerText = `$${pricePerCan.toFixed(2)}/can`; } else if (buyOnceAmountElement) { buyOnceAmountElement.innerText = ''; } // Only update the recurring price if a valid plan is found. // If not, the existing value remains, preventing it from disappearing. if (targetSellingPlanAllocation) { const priceAdjustments = targetSellingPlanAllocation.priceAdjustments[0]; const perDeliveryPrice = priceAdjustments.perDeliveryPrice; if (recurringPriceElement) recurringPriceElement.innerText = `$${perDeliveryPrice.amount}`; if (recurringAmountElement && numberOfCans > 0) { const pricePerCan = parseFloat(perDeliveryPrice.amount) / numberOfCans; recurringAmountElement.innerText = `$${pricePerCan.toFixed(2)}/can`; } } // --- FIX #1: The 'else' block that cleared the recurring price is removed. --- // By removing it, the recurring price will remain visible even when 'Buy Once' is selected. } // --- EVENT LISTENERS --- if (recurringPurchaseButton && singlePurchaseButton && sellingPlanContainer) { recurringPurchaseButton.addEventListener('click', () => { sellingPlanContainer.style.display = ''; // --- FIX #2: Re-evaluate and display the correct subscription price immediately. --- if (currentEl && currentVariant) { let defaultPlanId = null; try { const subStateAttr = currentEl.getAttribute( 'sf-current-subscription-state' ); if (subStateAttr) { const subscriptionState = JSON.parse(subStateAttr); if (subscriptionState?.[currentVariant.id]) { defaultPlanId = subscriptionState[currentVariant.id].sellingPlanId; } } } catch (e) { console.error('Error parsing subscription state on click:', e); } // Call the update function to ensure the recurring price is correct and visible. updatePriceDisplay(currentVariant, defaultPlanId); } }); singlePurchaseButton.addEventListener('click', () => { sellingPlanContainer.style.display = 'none'; // No need to call updatePriceDisplay here, as the prices should not change. // The UI state (active button) is handled by Shopyflow. }); } if (sellingPlanButtons.length > 0) { sellingPlanButtons.forEach(button => { button.addEventListener('click', () => { const clickedPlanId = button.getAttribute('sf-selling-plan-value'); updatePriceDisplay(currentVariant, clickedPlanId); }); }); } Shopyflow.on('optionChange', ({ el, variant }) => { // Update the globally accessible state variables. currentVariant = variant; currentEl = el; let initialPlanId = null; try { const subStateAttr = el.getAttribute('sf-current-subscription-state'); if (subStateAttr) { const subscriptionState = JSON.parse(subStateAttr); if (subscriptionState?.[variant.id]) { initialPlanId = subscriptionState[variant.id].sellingPlanId; } } } catch (e) { console.error('Error parsing sf-current-subscription-state:', e); } // Call the main update function. It will now correctly populate both one-time // and recurring prices without clearing either. updatePriceDisplay(currentVariant, initialPlanId); }); });