1/** 2 * Shared "Pay via Escrow.com" modal controller. 3 * 4 * Used by both the listing page (Buy Now) and the deal page (accepted offer). 5 * Exposes window.openEscrowModal({...}) so per-page handlers can trigger it 6 * without doing prompt+fetch themselves. 7 * 8 * Calling convention: 9 * 10 * openEscrowModal({ 11 * mode: 'listing' | 'transaction', 12 * listingId: <int> // when mode === 'listing' 13 * transactionId: <int> // when mode === 'transaction' 14 * triggerBtn: <HTMLElement> // optional; will be disabled while 15 * // the request is in flight 16 * }); 17 * 18 * On success, redirects window.location to Escrow.com's agreement URL. 19 * On failure, surfaces the error inside the modal and keeps it open. 20 * 21 * The user's last-used email is remembered in localStorage for next time. 22 */ 23(function () { 24 'use strict'; 25 26 var LS_KEY = 'mkt.escrowEmail'; 27 var LS_EXPLAINER_SEEN = 'mkt.escrowExplainerSeen'; 28 29 var modal, overlay, closeBtn, cancelBtn, continueBtn, emailInput, errorEl; 30 var explainerToggle, explainerPanel, explainerChevron, explainerLabel; 31 var currentMode = null; // 'listing' or 'transaction' 32 var currentListingId = null; 33 var currentTxnId = null; 34 var currentTrigger = null; 35 var origTriggerLabel = ''; 36 var initialized = false; 37 38 function init() { 39 if (initialized) return; 40 modal = document.getElementById('escrowModal'); 41 if (!modal) return; // page didn't include the partial; nothing to do 42 overlay = document.getElementById('escrowModalOverlay'); 43 closeBtn = document.getElementById('escrowModalClose'); 44 cancelBtn = document.getElementById('escrowModalCancel'); 45 continueBtn = document.getElementById('escrowModalContinue'); 46 emailInput = document.getElementById('escrowModalEmail'); 47 errorEl = document.getElementById('escrowModalError'); 48 49 // Close handlers 50 closeBtn.addEventListener('click', closeModal); 51 cancelBtn.addEventListener('click', closeModal); 52 overlay.addEventListener('click', closeModal); 53 document.addEventListener('keydown', function (e) { 54 if (e.key === 'Escape' && isOpen()) closeModal(); 55 }); 56 57 // Submit handlers 58 continueBtn.addEventListener('click', submit); 59 emailInput.addEventListener('keydown', function (e) { 60 if (e.key === 'Enter') { 61 e.preventDefault(); 62 submit(); 63 } 64 }); 65 66 // "What is Escrow.com?" explainer toggle (optional element) 67 explainerToggle = document.getElementById('escrowExplainerToggle'); 68 explainerPanel = document.getElementById('escrowExplainerPanel'); 69 explainerChevron = document.getElementById('escrowExplainerChevron'); 70 explainerLabel = document.getElementById('escrowExplainerToggleLabel'); 71 if (explainerToggle && explainerPanel) { 72 explainerToggle.addEventListener('click', function () { 73 setExplainer(explainerPanel.hidden); 74 }); 75 } 76 77 initialized = true; 78 } 79 80 function setExplainer(expanded) { 81 if (!explainerToggle || !explainerPanel) return; 82 explainerPanel.hidden = !expanded; 83 explainerToggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); 84 if (explainerChevron) { 85 explainerChevron.style.transform = expanded ? 'rotate(180deg)' : 'rotate(0deg)'; 86 } 87 if (explainerLabel) { 88 explainerLabel.textContent = expanded ? 'Hide details' : 'What is Escrow.com?'; 89 } 90 } 91 92 function isOpen() { 93 return modal && modal.style.display !== 'none' && modal.style.display !== ''; 94 } 95 96 function bestPrefill() { 97 // localStorage > server-rendered account email > empty. 98 try { 99 var fromLs = window.localStorage.getItem(LS_KEY); 100 if (fromLs) return fromLs; 101 } catch (e) { /* localStorage may throw in private mode */ } 102 return (modal && modal.dataset && modal.dataset.prefillEmail) || ''; 103 } 104 105 function openModal(opts) { 106 init(); 107 if (!modal) { 108 // No modal in this page â fall back to prompt() so we don't break 109 // the flow entirely. Should never happen if the partial is included. 110 var fallback = window.prompt('Enter your Escrow.com email:', bestPrefill()); 111 if (!fallback) return; 112 doRequest(fallback, opts); 113 return; 114 } 115 116 currentMode = opts.mode || null; 117 currentListingId = opts.listingId || null; 118 currentTxnId = opts.transactionId || null; 119 currentTrigger = opts.triggerBtn || null; 120 origTriggerLabel = currentTrigger ? currentTrigger.innerHTML : ''; 121 122 // Reset state on each open 123 hideError(); 124 setBusy(false); 125 emailInput.value = bestPrefill(); 126 127 // First-time visitors see the "What is Escrow.com?" panel expanded by 128 // default so they don't have to discover it. Returning visitors get 129 // the cleaner collapsed view. 130 var firstTime = false;
131 try { 132 firstTime = !window.localStorage.getItem(LS_EXPLAINER_SEEN); 133 if (firstTime) window.localStorage.setItem(LS_EXPLAINER_SEEN, '1'); 134 } catch (e) { /* localStorage may throw in private mode */ } 135 setExplainer(firstTime); 136 137 modal.style.display = 'flex'; 138 // Defer focus so the input is reachable after layout settles 139 setTimeout(function () { emailInput.focus(); emailInput.select(); }, 0); 140 } 141 142 function closeModal() { 143 if (!modal) return; 144 modal.style.display = 'none'; 145 // Restore trigger if we left it spinning 146 restoreTrigger(); 147 currentMode = null; 148 currentListingId = null; 149 currentTxnId = null; 150 currentTrigger = null; 151 } 152 153 function setBusy(busy) { 154 continueBtn.disabled = busy; 155 cancelBtn.disabled = busy; 156 emailInput.disabled = busy; 157 continueBtn.innerHTML = busy 158 ? '<i class="fas fa-spinner fa-spin"></i> Creating escrow transaction...' 159 : 'Continue to Escrow.com'; 160 } 161 162 function showError(msg) { 163 errorEl.textContent = msg; 164 errorEl.hidden = false; 165 } 166 167 function hideError() { 168 errorEl.textContent = ''; 169 errorEl.hidden = true; 170 } 171 172 function restoreTrigger() { 173 if (currentTrigger && origTriggerLabel) { 174 currentTrigger.disabled = false; 175 currentTrigger.innerHTML = origTriggerLabel; 176 } 177 } 178 179 function submit() { 180 hideError(); 181 var email = (emailInput.value || '').trim(); 182 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { 183 showError('Please enter a valid email address.'); 184 emailInput.focus(); 185 return; 186 } 187 188 setBusy(true); 189 if (currentTrigger) { 190 currentTrigger.disabled = true; 191 currentTrigger.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ...'; 192 } 193 194 doRequest(email, { 195 mode: currentMode, 196 listingId: currentListingId, 197 transactionId: currentTxnId 198 }); 199 } 200 201 function doRequest(email, opts) { 202 var payload = { buyer_email: email }; 203 if (opts.mode === 'transaction' && opts.transactionId) { 204 payload.transaction_id = opts.transactionId; 205 } else if (opts.mode === 'listing' && opts.listingId) { 206 payload.listing_id = opts.listingId; 207 } else { 208 setBusy(false); 209 showError('Internal error â missing listing or transaction id.'); 210 return; 211 } 212 213 fetch('/api/marketplace/escrow-checkout', { 214 method: 'POST', 215 headers: { 'Content-Type': 'application/json' }, 216 body: JSON.stringify(payload) 217 }).then(function (r) { 218 return r.json().then(function (d) { return { ok: r.ok, data: d }; }); 219 }).then(function (res) { 220 if (res.ok && res.data.success && res.data.agreement_url) { 221 // Remember the email for next time on this device. 222 try { window.localStorage.setItem(LS_KEY, email); } catch (e) { /* ignore */ } 223 window.location = res.data.agreement_url; 224 return; 225 } 226 setBusy(false); 227 showError((res.data && res.data.error) || 'Could not start the Escrow.com transaction. Please try again.'); 228 }).catch(function () { 229 setBusy(false); 230 showError('Network error. Please try again.'); 231 }); 232 } 233 234 // Expose to per-page handlers 235 window.openEscrowModal = openModal; 236 237 // Auto-init when DOM is ready (handles the case where this script is 238 // included after the partial but before the page's other handlers). 239 if (document.readyState === 'loading') { 240 document.addEventListener('DOMContentLoaded', init); 241 } else { 242 init(); 243 } 244})();
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.