From b61d5c34e37328175a5eff8ad0ccad03a5b8ef1d Mon Sep 17 00:00:00 2001 From: Olivier Lance Date: Thu, 24 Oct 2024 02:04:25 +0200 Subject: [PATCH 1/3] refactor: simplify & document insurance on product page --- alma/views/js/alma-product-insurance.js | 201 ++++++++++++++---------- 1 file changed, 122 insertions(+), 79 deletions(-) diff --git a/alma/views/js/alma-product-insurance.js b/alma/views/js/alma-product-insurance.js index dec14270..8bfafe83 100644 --- a/alma/views/js/alma-product-insurance.js +++ b/alma/views/js/alma-product-insurance.js @@ -26,49 +26,68 @@ throw new Error('[Alma] Product details not found. You need to add the hook displayProductActions in your template product page.'); } + let loaded = false; let insuranceSelected = false; let selectedAlmaInsurance = null; let addToCartFlow = false; let quantity = getQuantity(); let almaEligibilityAnswer = false; - //Insurance - $("body").on("hidden.bs.modal", "#blockcart-modal", function () { - removeInsurance(); - }); + // Reset the insurance widget & input when customer chooses "continue shopping" from the "added to cart" modal + $("body").on("hidden.bs.modal", "#blockcart-modal", removeInsurance); + + // Display warning to customer if they're seeing the insurance "product" page handleInsuranceProductPage(); - btnLoaders('start'); - onloadAddInsuranceInputOnProductAlma(); - if (typeof prestashop !== 'undefined') { - prestashop.on('updateProduct', function (event) { + + // Add spinner to add to cart button & disable it until insurance is loaded + showLoadingSpinner(); + + // Listening to messages from our widget + window.addEventListener('message', handleWidgetMessage); + + if (prestashop) { + prestashop.on('updateProduct', function (args) { let addToCart = getAddToCartButton(); - if (event.event !== undefined) { + // TODO: is this really useful? + if (args.event !== undefined) { quantity = getQuantity(); } - if (event.eventType === 'updatedProductQuantity') { - quantity = getQuantity(); - if (event.event) { - quantity = event.event.target.value; + + // Update quantity & reset insurance choices when quantity changes + if (args.eventType === 'updatedProductQuantity') { + if (args.event) { + quantity = Number(args.event.target.value); + } else { + quantity = getQuantity(); } removeInsurance(); } - if (event.eventType === 'updatedProductCombination') { + + // Reset insurance choice when product changes + if (args.eventType === 'updatedProductCombination') { removeInsurance(); } - if (typeof event.selectedAlmaInsurance !== 'undefined' && event.selectedAlmaInsurance !== null) { + + // An insurance offer has been chosen + if (Boolean(args.selectedAlmaInsurance)) { insuranceSelected = true; - addInputsInsurance(event); + addInsuranceInputs(args); } - if (typeof event.selectedInsuranceData !== 'undefined' && event.selectedInsuranceData) { - removeInputInsurance(); + + // Insurance choice has been withdrawn + if (Boolean(args.hasRemovedInsurance)) { + removeInsuranceInputs(); } + + // If we had intercepted the add to cart flow, resume it to effectively add the product to the cart if (addToCartFlow) { addToCart.click(); insuranceSelected = false; addToCartFlow = false; } }); + prestashop.on('updatedProduct', function (data) { // Update product details data from the PrestaShop-sent data if (data.product_details) { @@ -106,6 +125,7 @@ } + // Retrieve wanted product quantity from the quantity selector function getQuantity() { let quantity = 1; @@ -122,28 +142,36 @@ qtyInput.value = quantity; } - function btnLoaders(action) { - const $addBtn = $(".add-to-cart"); - if (action === 'start') { - $('
').insertBefore($(".add-to-cart i")); + // Display/hide a spinner on the add to cart button + function showLoadingSpinner(show = true) { + const $addBtn = $(getAddToCartButton()); + + if (show) { + loaded = false; + $addBtn.prepend($('
')); $addBtn.attr("disabled", "disabled"); - } - if (action === 'stop') { - $(".spinner").remove(); + } else { + $("#insuranceSpinner").remove(); $addBtn.removeAttr("disabled"); - addModalListenerToAddToCart(); } } - // ** Add input insurance in form to add to cart ** - function onloadAddInsuranceInputOnProductAlma() { - let currentResolve; + // Handle incoming messages from the widget + function handleWidgetMessage(message) { + let widgetInsurance = document.getElementById('alma-widget-insurance-product-page'); + + switch (message.data.type) { + // Widget is sending us the result of the eligibility call for the current product + case 'almaEligibilityAnswer': + almaEligibilityAnswer = message.data.eligibilityCallResponseStatus.response.eligibleProduct; + + // TODO: we should receive a "loaded" message from the widget + if (!loaded) { + loaded = true; + showLoadingSpinner(false); + addModalListenerToAddToCart(); + } - window.addEventListener('message', (e) => { - let widgetInsurance = document.getElementById('alma-widget-insurance-product-page'); - if (e.data.type === 'almaEligibilityAnswer') { - almaEligibilityAnswer = e.data.eligibilityCallResponseStatus.response.eligibleProduct; - btnLoaders('stop'); if (almaEligibilityAnswer) { prestashop.emit('updateProduct', { reason: { @@ -157,34 +185,37 @@ addToCart.removeEventListener("click", insuranceListener) } } - } - if (e.data.type === 'changeWidgetHeight') { - widgetInsurance.style.height = e.data.widgetHeight + 'px'; - } - if (e.data.type === 'getSelectedInsuranceData') { + break; + + // Widget is asking us to adjust the iframe's height + case 'changeWidgetHeight': + widgetInsurance.style.height = message.data.widgetHeight + 'px'; + break; + + // Widget is sending us selected insurance data + case 'getSelectedInsuranceData': if (parseInt(document.querySelector('.qty [name="qty"]').value) !== quantity) { quantity = getQuantity(); } insuranceSelected = true; - selectedAlmaInsurance = e.data.selectedInsuranceData; + selectedAlmaInsurance = message.data.selectedInsuranceData; prestashop.emit('updateProduct', { reason: { productUrl: window.location.href }, selectedAlmaInsurance: selectedAlmaInsurance, - selectedInsuranceData: e.data.declinedInsurance, - selectedInsuranceQuantity: e.data.selectedInsuranceQuantity + hasRemovedInsurance: message.data.declinedInsurance, + selectedInsuranceQuantity: message.data.selectedInsuranceQuantity }); - } else if (currentResolve) { - currentResolve(e.data); - } - }); + break; + } } + // Ask widget to refresh with updated product data function refreshWidget() { let cmsReference = createCmsReference(AlmaInsurance.productDetails); let priceAmount = AlmaInsurance.productDetails.price_amount; - if (AlmaInsurance.productDetails.price_amount === undefined) { + if (priceAmount === undefined) { priceAmount = AlmaInsurance.productDetails.price; } let staticPriceToCents = Math.round(priceAmount * 100); @@ -194,6 +225,7 @@ quantity = 1; } + // !! Global function provided by openInPageModal script getProductDataForApiCall( cmsReference, staticPriceToCents, @@ -206,20 +238,21 @@ ); } + // Concatenate product ID & its combination ID for a unique identifier function createCmsReference(productDetails) { if (!productDetails.id_product) { return } - // TODO: check why comparing to string value - if (productDetails.id_product_attribute <= '0') { + if (productDetails.id_product_attribute <= 0) { return productDetails.id_product; } return productDetails.id_product + '-' + productDetails.id_product_attribute; } - function addInputsInsurance(event) { + // Add hidden inputs to the add to cart form so that our insurance product is added along with the main product + function addInsuranceInputs(event) { let formAddToCart = document.getElementById('add-to-cart-or-refresh'); let selectedInsuranceQuantity = event.selectedInsuranceQuantity; @@ -227,37 +260,32 @@ selectedInsuranceQuantity = quantity } - handleInput('alma_id_insurance_contract', event.selectedAlmaInsurance.insuranceContractId, formAddToCart); - handleInput('alma_quantity_insurance', selectedInsuranceQuantity, formAddToCart); + // We need both the selected insurance contract ID, and how many subscriptions to add + addInsuranceInput('alma_id_insurance_contract', event.selectedAlmaInsurance.insuranceContractId, formAddToCart); + addInsuranceInput('alma_quantity_insurance', selectedInsuranceQuantity, formAddToCart); } - function handleInput(inputName, value, form) { - let elementInput = document.getElementById(inputName); - if (elementInput == null) { - let input = document.createElement('input'); - input.setAttribute('value', value); - input.setAttribute('name', inputName); - input.setAttribute('class', 'alma_insurance_input'); - input.setAttribute('id', inputName); - input.setAttribute('type', 'hidden'); - - form.prepend(input); + // Add a single insurance input to the add to cart form + function addInsuranceInput(inputName, value, form) { + let $elementInput = $(`#${inputName}`) + if (!$elementInput.length) { + const $input = $(``) + $(form).prepend($input); } else { - elementInput.setAttribute('value', value); + $elementInput.val(value); } } + // Remove our insurance hidden fields from the add to cart form + function removeInsuranceInputs() { + $('.alma_insurance_input').remove() + } + function removeInsurance() { + // !! Global function provided by openInPageModal script resetInsurance(); insuranceSelected = false; - removeInputInsurance(); - } - - function removeInputInsurance() { - let inputsInsurance = document.getElementById('add-to-cart-or-refresh').querySelectorAll('.alma_insurance_input'); - inputsInsurance.forEach((input) => { - input.remove(); - }); + removeInsuranceInputs(); } function addModalListenerToAddToCart() { @@ -271,27 +299,42 @@ } } - function getAddToCartButton() { - let addToCart = document.querySelector('button.add-to-cart'); - // TODO: Ravate specific to generalise with selector configuration - if (!addToCart) { - addToCart = document.querySelector('.add-to-cart a, .add-to-cart button').first(); - } + // Find the selector for the add to cart button + function getAddToCartBtnSelector() { + // TODO: move selectors to a module configuration option + const selectors = [ + '[data-button-action=add-to-cart]:visible', // generic PrestaShop add to cart button + 'button.add-to-cart:visible', + '.add-to-cart a:visible', + '.add-to-cart button:visible', + 'a[role=button][href$=addToCart]:visible' // elementor addToCart widget + ]; + + // Return first selector that successfully matches an element in the DOM + return selectors.find(selector => $(selector).length > 0) + } - return addToCart; + function getAddToCartButton() { + return $(getAddToCartBtnSelector())[0]; } function insuranceListener(event) { if (!insuranceSelected) { event.preventDefault(); event.stopPropagation(); + openModal('popupModal', quantity); + insuranceSelected = true; addToCartFlow = true; } + insuranceSelected = false; } + // This is only used to display a callout message when the product page for the actual insurance "product" from + // the catalog is being viewed by a customer + // TODO: This should probably be removed in favor of conditional templating/hooks on the product page itself function handleInsuranceProductPage() { const $almaInsuranceGlobal = $('#alma-insurance-global'); if (AlmaInsurance.productDetails.id === $almaInsuranceGlobal.data('insurance-id')) { From b5a7b6e91d8d1a36f8fac0f3fd9b81a6a92878b3 Mon Sep 17 00:00:00 2001 From: Olivier Lance Date: Fri, 25 Oct 2024 12:45:27 +0200 Subject: [PATCH 2/3] fix: include wanted product quantity in widget iframe URL Some versions of PS (e.g. 1.7.2.4) will completely reload the product actions DOM, causing the widget to fully reload and loose its state. As a result, the insurance modal always uses a quantity=1 when opened via the widget. Including the quantity in the URL fixes that --- .../controllers/hook/DisplayProductActionsHookController.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/alma/controllers/hook/DisplayProductActionsHookController.php b/alma/controllers/hook/DisplayProductActionsHookController.php index 8959309f..981cce9e 100644 --- a/alma/controllers/hook/DisplayProductActionsHookController.php +++ b/alma/controllers/hook/DisplayProductActionsHookController.php @@ -120,6 +120,8 @@ public function run($params) ? $productParams['id_product_attribute'] : null; + $quantityWanted = isset($productParams['quantity_wanted']) ? $productParams['quantity_wanted'] : 1; + $cmsReference = $this->insuranceHelper->createCmsReference($productId, $productAttributeId); $staticPrice = $this->productHelper->getPriceStatic($productId, $productAttributeId); @@ -132,11 +134,12 @@ public function run($params) 'productDetails' => $this->handleProductDetails($params), 'settingsInsurance' => $this->handleSettings($merchantId), 'iframeUrl' => sprintf( - '%s%s?cms_reference=%s&product_price=%s&product_name=%s&merchant_id=%s&customer_session_id=%s&cart_id=%s', + '%s%s?cms_reference=%s&product_price=%s&product_quantity=%s&product_name=%s&merchant_id=%s&customer_session_id=%s&cart_id=%s', $this->adminInsuranceHelper->envUrl(), ConstantsHelper::FO_IFRAME_WIDGET_INSURANCE_PATH, $cmsReference, $staticPriceInCents, + $quantityWanted, $productName, $merchantId, $this->context->cookie->checksum, From 50fb3349774fc144e6e10efcc1a10d02a27fae1b Mon Sep 17 00:00:00 2001 From: Olivier Lance Date: Fri, 25 Oct 2024 12:54:41 +0200 Subject: [PATCH 3/3] refactor: simplify widget & modal control flow PrestaShop events are not manually triggered anymore, as we really only need to listen to them, so that we can trigger some update flows. The rest of the control flow remains handled at the message event handler level, which removes unnecessary reloads and fixes a few other bugs --- alma/views/js/alma-product-insurance.js | 88 +++++++++++-------------- 1 file changed, 37 insertions(+), 51 deletions(-) diff --git a/alma/views/js/alma-product-insurance.js b/alma/views/js/alma-product-insurance.js index 8bfafe83..3711df75 100644 --- a/alma/views/js/alma-product-insurance.js +++ b/alma/views/js/alma-product-insurance.js @@ -47,8 +47,6 @@ if (prestashop) { prestashop.on('updateProduct', function (args) { - let addToCart = getAddToCartButton(); - // TODO: is this really useful? if (args.event !== undefined) { quantity = getQuantity(); @@ -68,24 +66,6 @@ if (args.eventType === 'updatedProductCombination') { removeInsurance(); } - - // An insurance offer has been chosen - if (Boolean(args.selectedAlmaInsurance)) { - insuranceSelected = true; - addInsuranceInputs(args); - } - - // Insurance choice has been withdrawn - if (Boolean(args.hasRemovedInsurance)) { - removeInsuranceInputs(); - } - - // If we had intercepted the add to cart flow, resume it to effectively add the product to the cart - if (addToCartFlow) { - addToCart.click(); - insuranceSelected = false; - addToCartFlow = false; - } }); prestashop.on('updatedProduct', function (data) { @@ -134,10 +114,18 @@ quantity = Number(qtyInput.value); } + if (quantity <= 0) { + quantity = 1; + } + return quantity } function setQuantity(quantity) { + if (quantity <= 0) { + quantity = 1; + } + const qtyInput = document.querySelector('.qty [name="qty"]'); qtyInput.value = quantity; } @@ -152,7 +140,7 @@ $addBtn.attr("disabled", "disabled"); } else { $("#insuranceSpinner").remove(); - $addBtn.removeAttr("disabled"); + $addBtn.attr("disabled", null); } } @@ -172,17 +160,11 @@ addModalListenerToAddToCart(); } - if (almaEligibilityAnswer) { - prestashop.emit('updateProduct', { - reason: { - productUrl: window.location.href, - } - }); - } else { + if (!almaEligibilityAnswer) { widgetInsurance.style.display = 'none'; let addToCart = document.querySelector('.add-to-cart'); if (addToCart) { - addToCart.removeEventListener("click", insuranceListener) + addToCart.removeEventListener("click", addToCartListener) } } break; @@ -197,16 +179,27 @@ if (parseInt(document.querySelector('.qty [name="qty"]').value) !== quantity) { quantity = getQuantity(); } - insuranceSelected = true; - selectedAlmaInsurance = message.data.selectedInsuranceData; - prestashop.emit('updateProduct', { - reason: { - productUrl: window.location.href - }, - selectedAlmaInsurance: selectedAlmaInsurance, + + const data = { + selectedAlmaInsurance: message.data.selectedInsuranceData, hasRemovedInsurance: message.data.declinedInsurance, selectedInsuranceQuantity: message.data.selectedInsuranceQuantity - }); + } + + if (Boolean(data.selectedAlmaInsurance)) { + // An insurance offer has been chosen + insuranceSelected = true; + addInsuranceInputs(data); + } else if (data.hasRemovedInsurance) { + // Insurance choice has been withdrawn + insuranceSelected = false; + removeInsuranceInputs(); + } + + // If we had intercepted the add to cart flow, resume it to effectively add the product to the cart + if (addToCartFlow) { + getAddToCartButton().click(); + } break; } } @@ -220,11 +213,6 @@ } let staticPriceToCents = Math.round(priceAmount * 100); - quantity = AlmaInsurance.productDetails.quantity_wanted; - if (quantity <= 0) { - quantity = 1; - } - // !! Global function provided by openInPageModal script getProductDataForApiCall( cmsReference, @@ -293,8 +281,8 @@ const addToCart = getAddToCartButton(); if (addToCart) { // If we change the quantity the DOM is reloaded then we need to remove and add the listener again - addToCart.removeEventListener("click", insuranceListener); - addToCart.addEventListener("click", insuranceListener); + addToCart.removeEventListener("click", addToCartListener); + addToCart.addEventListener("click", addToCartListener); } } } @@ -318,18 +306,16 @@ return $(getAddToCartBtnSelector())[0]; } - function insuranceListener(event) { - if (!insuranceSelected) { + function addToCartListener(event) { + if (!insuranceSelected && !addToCartFlow) { event.preventDefault(); event.stopPropagation(); - openModal('popupModal', quantity); - - insuranceSelected = true; addToCartFlow = true; + openModal('popupModal', quantity); + } else { + addToCartFlow = false; } - - insuranceSelected = false; } // This is only used to display a callout message when the product page for the actual insurance "product" from