1/** 2 * ETT Deposit Timer - persistent countdown widget. 3 * 4 * Draws the deposit hold countdown on EVERY page of the site while a hold is live, so the 5 * customer keeps seeing it even after browsing away from the payment page. 6 * 7 * Rendered entirely client-side, on purpose: the live site runs a full-page cache, so any 8 * server-rendered markup could be baked into cached HTML and shown to the wrong visitor. 9 * This script ships on every page and decides for itself, from the per-browser cookie, 10 * whether to draw anything. 11 * 12 * The cookie is only a hint for drawing. The real deadline lives on the booking, and 13 * ett_deposit_status (admin-ajax, never cached) is the authority: it re-syncs the 14 * remaining time on load and performs the cancellation once the hold is genuinely up. 15 * Editing the cookie can therefore change what this browser shows, but never what is 16 * charged or cancelled. 17 */ 18(function () { 19 'use strict'; 20 21 var CFG = window.ettDepositTimer || {}; 22 var COLLAPSE_KEY = 'ettDepositTimerCollapsed'; 23 var el = null, clock = null, bodyEl = null, toggleEl = null; 24 var remaining = 0, ticker = null, endsAt = 0, lastSync = 0; 25 26 // ââ helpers ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 27 28 function pad(n) { return (n < 10 ? '0' : '') + n; } 29 30 function readCookie() { 31 var name = (CFG.cookie || 'ett_deposit_timer') + '='; 32 var parts = document.cookie ? document.cookie.split(';') : []; 33 for (var i = 0; i < parts.length; i++) { 34 var c = parts[i].replace(/^\s+/, ''); 35 if (c.indexOf(name) !== 0) continue; 36 try { 37 return JSON.parse(atob(decodeURIComponent(c.substring(name.length)))); 38 } catch (e) { 39 return null; 40 } 41 } 42 return null; 43 } 44 45 function onPaymentFlow() { 46 var p = window.location.pathname || ''; 47 var q = window.location.search || ''; 48 return /\/(checkout|cart|order-received|order-pay)/i.test(p) || 49 (CFG.payFlag && q.indexOf(CFG.payFlag) !== -1); 50 } 51 52 function money(n) { 53 return String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ','); 54 } 55 56 function post(action, cb) { 57 if (!CFG.ajaxUrl) return; 58 try { 59 var xhr = new XMLHttpRequest(); 60 xhr.open('POST', CFG.ajaxUrl, true); 61 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 62 xhr.withCredentials = true; 63 xhr.onload = function () { 64 var res = null; 65 try { res = JSON.parse(xhr.responseText); } catch (e) {} 66 cb(res && res.success ? res.data : null); 67 }; 68 xhr.onerror = function () { cb(null); }; 69 xhr.send('action=' + encodeURIComponent(action)); 70 } catch (e) { cb(null); } 71 } 72 73 // ââ rendering ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 74 75 function remove() { 76 if (ticker) { clearInterval(ticker); ticker = null; } 77 if (el && el.parentNode) { el.parentNode.removeChild(el); } 78 el = null; 79 } 80 81 function shellStyle(accent) { 82 return 'position:fixed;right:16px;bottom:16px;z-index:999998;width:320px;' + 83 'max-width:calc(100vw - 32px);background:#fff;border:1px solid #f0b3ad;' + 84 'border-top:4px solid ' + accent + ';border-radius:8px;' + 85 'box-shadow:0 6px 24px rgba(0,0,0,0.22);font-family:sans-serif;color:#222;overflow:hidden;'; 86 } 87 88 function renderExpired() { 89 remove(); 90 var wa = CFG.whatsapp || ''; 91 var box = document.createElement('div'); 92 box.className = 'ett-deposit-timer-expired'; 93 box.style.cssText = shellStyle('#8a1c1c'); 94 box.innerHTML = 95 '<div style="padding:14px;">' + 96 '<div style="font-weight:700;font-size:14px;color:#8a1c1c;margin-bottom:6px;">Booking hold expired</div>' + 97 '<div style="font-size:12.5px;line-height:1.55;">We could not confirm your deposit in time, so your ' + 98 'booking has been suspended. If you had any trouble paying, message us on WhatsApp ' + 99 '<a href="' + (CFG.waLink || '#') + '" target="_blank" rel="noopener noreferrer" ' + 100 'style="color:#8a1c1c;font-weight:700;">
100' + wa + '</a> ' + 101 'and we will get your places back.</div>' + 102 '</div>'; 103 document.body.appendChild(box); 104 el = box; 105 } 106 107 function render(data) { 108 remove(); 109 var wa = CFG.whatsapp || ''; 110 var url = data.url || ''; 111 var amount = parseFloat(data.amount || 0); 112 var away = !onPaymentFlow(); 113 114 var box = document.createElement('div'); 115 box.className = 'ett-deposit-timer'; 116 box.style.cssText = shellStyle('#c0392b'); 117 box.innerHTML = 118 '<div class="ett-dt-head" style="display:flex;align-items:center;justify-content:space-between;' + 119 'padding:10px 14px;background:#fdecea;border-bottom:1px solid #f0b3ad;cursor:pointer;">' + 120 '<span style="font-weight:700;font-size:13px;color:#8a1c1c;">Deposit required to hold your booking</span>' + 121 '<span class="ett-dt-toggle" style="font-size:16px;line-height:1;color:#8a1c1c;padding-left:8px;">−</span>' + 122 '</div>' + 123 '<div class="ett-dt-body" style="padding:14px;">' + 124 '<div style="text-align:center;margin-bottom:10px;">' + 125 '<div style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#666;">Time remaining</div>' + 126 '<div class="ett-dt-clock" style="font-size:30px;font-weight:800;color:#c0392b;line-height:1.2;">--:--</div>' + 127 '</div>' + 128 '<div style="font-size:12.5px;line-height:1.55;color:#333;">Your booking will be ' + 129 '<strong>automatically revoked</strong> if the deposit' + 130 (amount > 0 ? ' of <strong>' + money(amount) + ' GEL</strong>' : '') + 131 ' is not paid before the timer runs out.</div>' + 132 (away && url ? 133 '<div class="ett-dt-away" style="margin-top:10px;padding:9px 11px;background:#fff8e6;' + 134 'border:1px solid #e8c98a;border-radius:5px;font-size:12.5px;line-height:1.5;">' + 135 'You have left the payment page. <a href="' + url + '" style="color:#8a1c1c;font-weight:700;">' + 136 'Return to payment</a> to finish your deposit.</div>' : '') + 137 (away && url ? '<a href="' + url + '" style="display:block;margin-top:12px;padding:11px 14px;' + 138 'background:#c0392b;color:#fff;border-radius:5px;font-weight:700;font-size:14px;' + 139 'text-align:center;text-decoration:none;">Pay deposit now</a>' : '') + 140 '<div style="margin-top:11px;padding-top:10px;border-top:1px solid #eee;font-size:12px;' + 141 'line-height:1.5;color:#555;">Trouble paying? Contact us on WhatsApp ' + 142 '<a href="' + (CFG.waLink || '#') + '" target="_blank" rel="noopener noreferrer" ' + 143 'style="color:#8a1c1c;font-weight:700;">' + wa + '</a>' + 144 ' and we will help.</div>' + 145 '</div>'; 146 147 document.body.appendChild(box); 148 el = box; 149 clock = box.querySelector('.ett-dt-clock'); 150 bodyEl = box.querySelector('.ett-dt-body'); 151 toggleEl = box.querySelector('.ett-dt-toggle'); 152 153 // Collapsed state persists across pages, so the widget can be tucked away without 154 // ever disappearing entirely. 155 var collapsed = false; 156 try { collapsed = sessionStorage.getItem(COLLAPSE_KEY) === '1'; } catch (e) {} 157 applyCollapsed(collapsed); 158 159 var head = box.querySelector('.ett-dt-head'); 160 if (head) { 161 head.addEventListener('click', function () { 162 collapsed = !collapsed; 163 applyCollapsed(collapsed); 164 try { sessionStorage.setItem(COLLAPSE_KEY, collapsed ? '1' : '0'); } catch (e) {} 165 }); 166 } 167 168 paint(); 169 ticker = setInterval(tick, 1000); 170 } 171 172 function applyCollapsed(collapsed) { 173 if (!bodyEl || !toggleEl) return; 174 bodyEl.style.display = collapsed ? 'none' : ''; 175 toggleEl.innerHTML = collapsed ? '+' : '−'; 176 } 177 178 function paint() { 179 if (!clock) return; 180 if (remaining <= 0) { clock.textContent = '00:00'; return; } 181 clock.textContent = pad(Math.floor(remaining / 60)) + ':' + pad(remaining % 60); 182 if (remaining <= 300) { clock.style.color = '#8a1c1c'; } 183 } 184 185 /** 186 * Derive the remaining time from a fixed end timestamp rather than counting ticks down. 187 * A backgrounded tab throttles (or entirely suspends) setInterval, so a decrementing 188 * counter silently loses time and looks paused. Reading the clock each tick means the 189 * display is always the true remaining time the moment the tab is looked at again. 190 */ 191 function tick() { 192 remaining = Math.max(0, Math.round((endsAt - Date.now()) / 1000)); 193 paint(); 194 if (remaining <= 0) { 195 if (ticker) { clearInterval(ticker); ticker = null; } 196 // Time is up locally: the server decides (it may have been paid meanwhile). 197 post('ett_deposit_status', function (d) { 198 if (d && d.status === 'active') { start(d); return; } 199 if (d && d.status === 'paid') { remove(); return; } 200 renderExpired(); 201 }); 202 } 203 } 204
205 /** Re-sync against the server, which owns the real deadline. */ 206 function sync(force) { 207 var now = Date.now(); 208 if (!force && now - lastSync < 5000) return; // avoid hammering on rapid tab flips 209 lastSync = now; 210 post('ett_deposit_status', function (d) { 211 if (!d) return; 212 if (d.status === 'active') { start(d); } 213 else if (d.status === 'paid') { remove(); } 214 else if (d.status === 'expired') { renderExpired(); } 215 }); 216 } 217 218 function start(data) { 219 remaining = parseInt(data.remaining, 10); 220 if (isNaN(remaining) || remaining <= 0) { renderExpired(); return; } 221 endsAt = Date.now() + (remaining * 1000); 222 render(data); 223 } 224 225 // ââ boot âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 226 227 function init() { 228 if (!readCookie()) return; // no hold for this browser: draw nothing 229 230 // Always confirm with the server before drawing, so a stale cookie (paid in another 231 // tab, cancelled by an agent, deadline already gone) never shows a live countdown. 232 post('ett_deposit_status', function (d) { 233 if (!d) return; 234 if (d.status === 'active') { start(d); } 235 else if (d.status === 'expired') { renderExpired(); } 236 }); 237 } 238 239 document.addEventListener('visibilitychange', function () { 240 if (document.visibilityState === 'visible' && el) { sync(false); } 241 }); 242 window.addEventListener('focus', function () { if (el) { sync(false); } }); 243 244 if (document.readyState === 'loading') { 245 document.addEventListener('DOMContentLoaded', init); 246 } else { 247 init(); 248 } 249})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.