What this does
Live pricing on your webstore depends on two systems - your ERP and EvolutionX - communicating successfully in real time. Occasionally that handshake fails. When it does, this script shows a modal that tells the customer live pricing couldn't be retrieved, and gives them a Try Again button that reloads the page.
It's a small addition, but it helps to stop customers from silently seeing stale or missing prices and gives them an obvious next step.
Installation
Log in to your store admin.
Go to Appearance > Theme Options.
Locate the Footer Block field.
Paste the script below in its entirety, on a new row.
Click Publish
Access to Theme Options depends on each admin user's permissions - if you can't see it, check with whoever manages store admin access.
Paste the whole script, unedited apart from the wording described below. A partial paste is a common cause of the modal not appearing.
The script
<script>
(function (window, document, $) {
'use strict';
var CONFIG = {
title: 'Pricing Unavailable',
heading: "Pricing isn't available right now",
message: "We couldn't retrieve live pricing. Please try again.",
cancelLabel: 'Cancel',
retryLabel: 'Try Again',
closeLabel: 'Close',
showOncePerPage: true,
showDelay: 150,
debug: false
};
var LIVE_PRICING_URL = '/ajax/live-pricing';
var FAILED_BODY_CLASS = 'ex-livepricing-failed';
var NS = 'evox-lp-modal';
var state = {
shown: false,
open: false,
lastFocused: null
};
function log() {
if (CONFIG.debug && window.console) {
console.log.apply(console, ['[LivePricingModal]'].concat([].slice.call(arguments)));
}
}
function injectStyles() {
if (document.getElementById(NS + '-styles')) return;
var css = '' +
'.' + NS + '-overlay{position:fixed;inset:0;top:0;right:0;bottom:0;left:0;z-index:2147483000;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.5);-webkit-overflow-scrolling:touch;overflow-y:auto}' +
'.' + NS + '-overlay.is-open{display:flex}' +
'.' + NS + '-dialog{position:relative;box-sizing:border-box;width:100%;max-width:562px;margin:auto;background:#fff;border-radius:4px;box-shadow:0 10px 40px rgba(0,0,0,.25);font-family:inherit;color:#000;text-align:left}' +
'.' + NS + '-dialog *{box-sizing:border-box}' +
'.' + NS + '-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid #d4d7dd;border-radius:4px 4px 0 0}' +
'.' + NS + '-title{margin:0;font-size:20px;font-weight:600;line-height:normal;color:#000}' +
'.' + NS + '-close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;margin:-4px -8px -4px 0;padding:0;border:0;background:transparent;color:#4c4c4c;cursor:pointer;border-radius:4px}' +
'.' + NS + '-close:hover,.' + NS + '-close:focus{color:#000;background:#f2f3f5;outline:none}' +
'.' + NS + '-close svg{width:16px;height:16px;display:block}' +
'.' + NS + '-body{display:flex;flex-direction:column;align-items:center;gap:24px;padding:32px 32px 24px;text-align:center}' +
'.' + NS + '-icon{width:72px;height:72px;flex:0 0 72px}' +
'.' + NS + '-icon svg{width:100%;height:100%;display:block}' +
'.' + NS + '-text{display:flex;flex-direction:column;gap:8px;width:100%}' +
'.' + NS + '-heading{margin:0;font-size:20px;font-weight:600;line-height:normal;color:#000}' +
'.' + NS + '-message{margin:0;font-size:16px;font-weight:400;line-height:normal;color:#4c4c4c}' +
'.' + NS + '-footer{display:flex;gap:16px;padding:16px 24px 24px}' +
'.' + NS + '-footer .btn{flex:1 1 0;min-width:0;padding:12px 24px;font-size:14px;font-weight:500;line-height:16px;text-align:center;border-radius:4px;cursor:pointer;white-space:nowrap}' +
'.' + NS + '-cancel{background:#fff;border:1px solid #d4d7dd;color:#000}' +
'.' + NS + '-cancel:hover,.' + NS + '-cancel:focus{background:#f7f8fa;color:#000}' +
'body.' + NS + '-lock{overflow:hidden}' +
'@media (max-width:480px){.' + NS + '-footer{flex-direction:column-reverse}' +
'.' + NS + '-body{padding:24px 20px 16px}' +
'}';
var style = document.createElement('style');
style.id = NS + '-styles';
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
function escapeHtml(str) {
return String(str)
.split('&').join('&' + 'amp;')
.split('<').join('&' + 'lt;')
.split('>').join('&' + 'gt;')
.split('"').join('&' + 'quot;')
.split("'").join('&' + '#39;');
}
var ICON_CLOSE = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M2 2l12 12M14 2L2 14"/></svg>';
var ICON_CANCEL = '<svg viewBox="0 0 72 72" fill="none" aria-hidden="true" focusable="false">' +
'<circle cx="36" cy="36" r="31" stroke="#E5322D" stroke-width="5"/>' +
'<path d="M25 25l22 22M47 25L25 47" stroke="#E5322D" stroke-width="5" stroke-linecap="round"/>' +
'</svg>';
function buildModal() {
var existing = document.getElementById(NS);
if (existing) return existing;
var overlay = document.createElement('div');
overlay.id = NS;
overlay.className = NS + '-overlay';
overlay.setAttribute('role', 'presentation');
overlay.innerHTML = '' +
'<div class="' + NS + '-dialog" role="dialog" aria-modal="true" aria-labelledby="' + NS + '-title" aria-describedby="' + NS + '-message">' +
'<div class="' + NS + '-header">' +
'<h2 class="' + NS + '-title" id="' + NS + '-title">' + escapeHtml(CONFIG.title) + '</h2>' +
'<button type="button" class="' + NS + '-close" data-action="close" aria-label="' + escapeHtml(CONFIG.closeLabel) + '">' + ICON_CLOSE + '</button>' +
'</div>' +
'<div class="' + NS + '-body">' +
'<div class="' + NS + '-icon">' + ICON_CANCEL + '</div>' +
'<div class="' + NS + '-text">' +
'<p class="' + NS + '-heading">' + escapeHtml(CONFIG.heading) + '</p>' +
'<p class="' + NS + '-message" id="' + NS + '-message">' + escapeHtml(CONFIG.message) + '</p>' +
'</div>' +
'</div>' +
'<div class="' + NS + '-footer">' +
'<button type="button" class="btn ' + NS + '-cancel" data-action="close">' + escapeHtml(CONFIG.cancelLabel) + '</button>' +
'<button type="button" class="btn btn-primary ' + NS + '-retry" data-action="retry">' + escapeHtml(CONFIG.retryLabel) + '</button>' +
'</div>' +
'</div>';
document.body.appendChild(overlay);
overlay.addEventListener('click', function (e) {
var actionEl = e.target.closest ? e.target.closest('[data-action]') : null;
if (actionEl) {
if (actionEl.getAttribute('data-action') === 'retry') {
retry();
} else {
closeModal();
}
return;
}
if (e.target === overlay) {
closeModal();
}
});
return overlay;
}
function onKeydown(e) {
if (!state.open) return;
if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
closeModal();
return;
}
if (e.key === 'Tab' || e.keyCode === 9) {
var overlay = document.getElementById(NS);
if (!overlay) return;
var focusable = overlay.querySelectorAll('button:not([disabled]),[href],input:not([disabled]),[tabindex]:not([tabindex="-1"])');
if (!focusable.length) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
function openModal() {
if (state.open) return;
if (CONFIG.showOncePerPage && state.shown) {
log('already shown on this page load, skipping');
return;
}
injectStyles();
var overlay = buildModal();
state.shown = true;
state.open = true;
state.lastFocused = document.activeElement;
overlay.classList.add('is-open');
document.body.classList.add(NS + '-lock');
document.addEventListener('keydown', onKeydown);
var retryBtn = overlay.querySelector('.' + NS + '-retry');
if (retryBtn) retryBtn.focus();
document.dispatchEvent(new CustomEvent('live-pricing-modal-shown'));
log('modal opened');
}
function closeModal() {
if (!state.open) return;
var overlay = document.getElementById(NS);
state.open = false;
if (overlay) overlay.classList.remove('is-open');
document.body.classList.remove(NS + '-lock');
document.removeEventListener('keydown', onKeydown);
if (state.lastFocused && typeof state.lastFocused.focus === 'function') {
try { state.lastFocused.focus(); } catch (err) { /* noop */ }
}
state.lastFocused = null;
document.dispatchEvent(new CustomEvent('live-pricing-modal-closed'));
log('modal closed');
}
function retry() {
var retryBtn = document.querySelector('.' + NS + '-retry');
if (retryBtn) {
retryBtn.disabled = true;
retryBtn.setAttribute('aria-busy', 'true');
}
window.location.reload();
}
function scheduleShow(reason) {
log('live pricing failure detected:', reason);
window.setTimeout(openModal, CONFIG.showDelay);
}
function isLivePricingRequest(settings) {
var url = settings && settings.url ? String(settings.url) : '';
if (!url) return false;
var path = url.replace(/^[a-z]+:\/\/[^\/]+/i, '');
return path.indexOf(LIVE_PRICING_URL) === 0;
}
function parseJson(xhr) {
if (xhr && xhr.responseJSON) return xhr.responseJSON;
try {
return JSON.parse(xhr.responseText);
} catch (err) {
return null;
}
}
function inspectResponse(xhr) {
if (!xhr || xhr.status < 200 || xhr.status >= 300) {
return 'http ' + (xhr ? xhr.status : 'n/a');
}
var json = parseJson(xhr);
if (!json || json.status !== 'success') {
return 'response status ' + (json ? json.status : 'unparseable');
}
if (json.livepricing_is_successful === false) {
return 'livepricing_is_successful=false';
}
return null;
}
function bindDetection() {
if ($ && $.fn && $(document).on) {
$(document).on('ajaxComplete.' + NS, function (event, xhr, settings) {
if (!isLivePricingRequest(settings)) return;
var reason = inspectResponse(xhr);
if (reason) scheduleShow(reason);
});
}
document.addEventListener('live-pricing-done', function () {
if (document.body.classList.contains(FAILED_BODY_CLASS)) {
scheduleShow('body.' + FAILED_BODY_CLASS + ' after live-pricing-done');
}
}, false);
if (window.MutationObserver) {
var observer = new MutationObserver(function () {
if (document.body.classList.contains(FAILED_BODY_CLASS)) {
scheduleShow('body.' + FAILED_BODY_CLASS + ' via MutationObserver');
observer.disconnect();
}
});
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
}
if (document.body.classList.contains(FAILED_BODY_CLASS)) {
scheduleShow('body.' + FAILED_BODY_CLASS + ' present at init');
}
}
function init() {
if (!document.body.classList.contains('has-live-pricing')) {
log('store does not have live pricing, not binding');
return;
}
bindDetection();
log('bound');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.EvoXLivePricingModal = {
open: openModal,
close: closeModal,
retry: retry,
config: CONFIG
};
})(window, document, window.jQuery);
</script>
Combine with a fallback
We'd recommend pairing this modal with an additional platform fallback for cases where a customer closes the modal without retrying. See Live Pricing Fallback for how to set that up.
FAQs
Why does live pricing fail?
It can fail for a number of reasons, since live pricing requires two platforms - your ERP and EvolutionX -to communicate successfully. Failure rates are low overall, but heavier traffic can cause spikes. This modal lets customers retry and continue with their order rather than getting stuck.
Can I change the content of the modal?
You can change the heading, message, and button labels by editing the text between the quote marks in the CONFIG block near the top of the script. Everything else - the quote marks, commas, and surrounding structure -needs to stay exactly as it is, or the modal is likely to stop working.
Setting | Controls |
heading | Bold heading inside the modal |
message | Supporting message under the heading |
cancelLabel | Text on the cancel button |
retryLabel | Text on the retry button |
Only edit text between the quote marks. Removing a quote mark, comma, or brace elsewhere in that block is the most common cause of the modal failing to load.

