1'use strict'; 2 3/** 4 * Location picker â 7-step modal mirrored from CI3 wla_home_20260507.js. 5 * All API calls go to AppConfig.apiBase (https://api.uengage.in). 6 * Loaded on every page from base.php; auto-opens when AppConfig.autoOpenLocationModal=1 7 * AND localStorage has no userLat/userLng. Header pill is wired site-wide. 8 * 9 * Globals exposed (matching CI3 names referenced from modal markup): 10 * closeLocationFlow, CurrentAddress, showLocationStep, 11 * enterAddressSearchLocality, homeSearchLocality, 12 * openHomeAddressForm, homeAddrGoToCurrentLocation, 13 * confirmLocationAndShowForm, saveHomeAddress, 14 * selectOrderTypeAndRedirect, openLoginOrSavedAddresses, changeUserLocation 15 */ 16(function () { 17 var AppConfig = window.AppConfig || {}; 18 var API_BASE = (AppConfig.apiBase || '').replace(/\/$/, ''); 19 var API_KEY = AppConfig.staticApiKey || ''; 20 var pId = Number(AppConfig.businessId || 0); 21 var mapsKey = AppConfig.mapsKey || ''; 22 var mapsFlow = Number(AppConfig.mapsFlow || 0); 23 var useGoogle = Boolean(mapsKey) && mapsFlow === 1; 24 25 function apiHeaders(extra) { 26 var h = { 'api-key': API_KEY }; 27 if (extra) Object.keys(extra).forEach(function (k) { h[k] = extra[k]; }); 28 return h; 29 } 30 31 // âââ Reset & expiry âââââââââââââââââââââââââââââââââââââ 32 // Every localStorage key the location flow writes â used by logout cleanup and TTL expiry. 33 var _LP_LOCATION_KEYS = [ 34 'userLat', 'userLng', 'userAddress', 'userAddressLabel', 'userLocSource', 35 'slug', 'business', 'locality', 'city', 'orderType', 36 'deliveryAddressId', 'selectedDeliveryAddressId', 'selectedAddressId', 37 'formatted_Address', 'locationSelectedAt', 38 // Pre-booking: outlet name cache + chosen slot â clear on logout/TTL too. 39 'pbkOutletName', 'pbkOutletId', 40 'slot_id', 'slot_date', 'slot_label', 'slot_startH', 'slot_endH' 41 ]; 42 var _LP_TTL_MS = 60 * 60 * 1000; // 60 minutes â re-prompt for location after this 43 44 // Public reset hook â logout handlers (profile_sidebar.js, userProfile.js) call this. 45 window.resetLocationJourney = function () { 46 _LP_LOCATION_KEYS.forEach(function (k) { localStorage.removeItem(k); }); 47 // Also clear per-user cart/order/address caches that must not survive logout: 48 // packs (pkgCartMeta/pkgOrderId), subscriptions (subCartMeta/subOrderId), and the 49 // checkout-address cache co_addr_{pId}_{contactId} (dynamic key â prefix scan). 50 ['pkgCartMeta', 'pkgOrderId', 'subCartMeta', 'subOrderId'].forEach(function (k) { localStorage.removeItem(k); }); 51 try { 52 Object.keys(localStorage).forEach(function (k) { if (k.indexOf('co_addr_') === 0) localStorage.removeItem(k); }); 53 } catch (e) { /* ignore */ } 54 // Clear the view_business cookie too â it gates which outlet's menu/cart loads. 55 document.cookie = 'view_business=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; 56 }; 57 58 // TTL: if the user confirmed an outlet more than 60 mins ago, wipe the journey so the 59 // modal re-prompts on the next page. Runs once per page load before maybeAutoOpen(). 60 (function _checkLocationTtl() { 61 var ts = parseInt(localStorage.getItem('locationSelectedAt') || '0', 10); 62 if (ts && (Date.now() - ts) > _LP_TTL_MS) window.resetLocationJourney(); 63 })(); 64 65 // âââ Location flow disabled (location_popup_config = 0) ââââââââââââââââââ 66 // Replaces CI3 wla_home_20260608.js isSingleOutlet block. 67 // Single outlet â seed slug + navigate directly to /order/{slug}. 68 // Multi outlet â send to /online-order (store locator) to pick first. 69 if (!Number(AppConfig.locationPopupConfig)) { 70 var isSingle = Number(AppConfig.storesCount) === 1; 71 72 if (isSingle) { 73 // Seed the outlet from home page storesDetails into localStorage. 74 // OVERWRITE unconditionally: with one outlet there is nothing for the 75 // user to choose, so a previously stored slug is a stale cache, not a 76 // selection to preserve. Leaving it in place strands the visitor â 77 // /order/{unknown-slug} 302s back to home, which reads as "the category 78 // click just reloads the page". Renamed outlets and slugs carried over 79 // from an earlier config both land here. city/locality are evicted 80 // before the conditional writes so an outlet whose city/locality is 81 // blank clears the stale values rather than leaving them sitting next 82 // to a freshly written slug. 83 var _singleOutletSlug = (function _seedDisabledOutlet() { 84 var d = window.storesDetails; 85 if (!d || !d.length) return ''; 86 var store = d[0]; 87 var slug = store.web_slug || store.slug || ''; 88 if (!slug) return ''; 89 localStorage.setItem('slug', slug);
90 localStorage.removeItem('city'); 91 localStorage.removeItem('locality'); 92 if (store.city) localStorage.setItem('city', store.city); 93 if (store.locality) localStorage.setItem('locality', store.locality); 94 return slug; 95 })(); 96 97 // Server-rendered slug wins over localStorage for the same reason. 98 // Both handlers take the outlet slug as their last argument so neither 99 // depends on window.storesDetails being present on the page. 100 window.silverCatClick = function (sectionSlug, outletSlug) { 101 var slug = outletSlug || _singleOutletSlug || localStorage.getItem('slug') || ''; 102 if (!slug || slug === 'null') return; 103 var url = '/order/' + slug; 104 if (sectionSlug) url += '?section_slug=' + encodeURIComponent(sectionSlug); 105 window.location.href = url; 106 }; 107 window.silverItemClick = function (sectionSlug, itemSlug, storeCount, outletSlug) { 108 var slug = outletSlug || _singleOutletSlug || localStorage.getItem('slug') || ''; 109 if (!slug || slug === 'null') return; 110 var url = '/order/' + slug; 111 var qs = []; 112 if (sectionSlug) qs.push('section_slug=' + encodeURIComponent(sectionSlug)); 113 if (itemSlug) qs.push('item_slug=' + encodeURIComponent(itemSlug)); 114 if (qs.length) url += '?' + qs.join('&'); 115 window.location.href = url; 116 }; 117 // The header's "Deliver to" chip calls changeUserLocation(), which is 118 // only assigned near the bottom of this module â past the return 119 // below. A single-outlet brand therefore had an undefined global and 120 // the chip did nothing at all when clicked. There is no journey to 121 // open here (no #locationFlowModal is rendered either), and the 122 // established behaviour for one outlet is to go straight to its menu, 123 // which is exactly what silverCatClick does above. 124 window.changeUserLocation = function () { 125 var slug = _singleOutletSlug || localStorage.getItem('slug') || ''; 126 if (!slug || slug === 'null') return; 127 window.location.href = '/order/' + slug; 128 }; 129 130 // The chip reads localStorage on its own (deferred) script run, which 131 // can happen before the seeding above. Nudge it so it shows the outlet 132 // rather than sitting on "Set location" for a brand where a location 133 // cannot be set. No-op when the v2 header is not the one rendered. 134 if (typeof window.hdr2RefreshHeader === 'function') window.hdr2RefreshHeader(); 135 } else { 136 // Multi-outlet with flow disabled: send to store locator to pick an outlet. 137 window.silverCatClick = function () { window.location.href = '/online-order'; }; 138 window.silverItemClick = function () { window.location.href = '/online-order'; }; 139 window.changeUserLocation = function () { window.location.href = '/online-order'; }; 140 } 141 return; // no modal, no info bar â nothing else to initialise 142 } 143 144 var modalEl = document.getElementById('locationFlowModal'); 145 if (!modalEl) return; 146 147 var state = { 148 lat: null, lng: null, 149 address: '', locality: '', city: '', stateName: '', pinCode: '', 150 nearest: [], currentStep: 'location', pendingAddr: null,
151 // Promise for an in-flight add/update-address POST. selectOrderTypeAndRedirect awaits 152 // it (bounded) so the freshly-created deliveryAddressId is committed before redirect. 153 savePromise: null 154 }; 155 window._lpPrevStep = 'location'; 156 157 function userData() { 158 try { return JSON.parse(localStorage.getItem('userdata') || '{}') || {}; } catch (e) { return {}; } 159 } 160 161 // Write the canonical delivery-address selection the checkout page reads 162 // (checkout.js: co_addr_{parentId}_{contactId} â {id,text,name}). pId here is 163 // the parent id, the same brand scope checkout uses, so the address chosen in 164 // this flow is restored on checkout without asking the user to pick again. 165 function persistCheckoutAddress(id, text, name) { 166 var ud = userData(); 167 if (!ud.contactId || !id || String(id) === '0') return; 168 try { 169 localStorage.setItem('co_addr_' + pId + '_' + ud.contactId, 170 JSON.stringify({ id: String(id), text: text || '', name: name || '' })); 171 } catch (e) {} 172 } 173 174 // âââ Deferred-address buffer ââââââââââââââââââââââââââââ 175 // While the user is mid-flow â they've picked/added/searched an address but have NOT 176 // yet been redirected to a real outlet WITH an order type â the selection is held in 177 // memory only. Nothing touches the keys checkout reads (co_addr_*, deliveryAddressId, 178 // userAddress). flushPendingAddr() commits it from selectOrderTypeAndRedirect; an 179 // abandoned flow ("not serviceable" â Explore Menu, or just closing the modal) leaves 180 // no stray address on the checkout page. 181 function setPendingAddr(a) { state.pendingAddr = a; } 182 function clearPendingAddr() { state.pendingAddr = null; } 183 function flushPendingAddr() { 184 var a = state.pendingAddr; 185 if (!a) return; 186 if (a.text) localStorage.setItem('userAddress', a.text); 187 // An EXPLICIT empty label clears the key instead of being skipped. These guards 188 // can only ever set a value, never clear one, so a caller that deliberately has 189 // no address type â the GPS path passes label: '' and says so in a comment â 190 // silently inherited the previous selection's label. `undefined` still means 191 // "this caller isn't touching the label"; '' means "there is no label". 192 if (a.label) localStorage.setItem('userAddressLabel', a.label); 193 else if (a.label === '') localStorage.removeItem('userAddressLabel'); 194 if (a.locality) localStorage.setItem('locality', a.locality); 195 if (a.city) localStorage.setItem('city', a.city); 196 if (a.id && String(a.id) !== '0') { 197 localStorage.setItem('deliveryAddressId', String(a.id)); 198 localStorage.setItem('selectedDeliveryAddressId', String(a.id)); 199 persistCheckoutAddress(a.id, a.text || '', a.label || 'Home'); 200 } else { 201 // A new location was chosen but no saved-address id resolved (add-address upstream 202 // returned no id, or the user picked a raw locality). The previously stored 203 // deliveryAddressId points at a DIFFERENT, older address â clear it so checkout 204 // can't silently reuse the wrong one for this freshly chosen location. 205 var ud = userData(); 206 localStorage.removeItem('deliveryAddressId'); 207 localStorage.removeItem('selectedDeliveryAddressId'); 208 if (ud.contactId) localStorage.removeItem('co_addr_' + pId + '_' + ud.contactId); 209 } 210 state.pendingAddr = null; 211 } 212 function $(sel) { return modalEl.querySelector(sel); } 213 function $$(sel) { return modalEl.querySelectorAll(sel); } 214 215 // âââ Modal show/hide via Bootstrap ââââââââââââââââââââââ 216 var bsModal = null; 217 function getModal() { 218 if (bsModal) return bsModal; 219 if (window.bootstrap && bootstrap.Modal) { 220 bsModal = bootstrap.Modal.getOrCreateInstance(modalEl, { backdrop: 'static', keyboard: true }); 221 } 222 return bsModal; 223 } 224 function showOverlay() { 225 // Forced mode (window._lpForceSelect): the caller (e.g. pre-booking detail) 226 // requires a location before proceeding â hide the close buttons so the modal 227 // can't be dismissed. Escape/backdrop are blocked via the hide.bs.modal guard 228 // below + the existing static backdrop. Default off â normal dismissible modal. 229 var force = !!window._lpForceSelect; 230 modalEl.querySelectorAll('.lp-brandhead__close').forEach(function (b) { b.style.display = force ? 'none' : ''; }); 231 var m = getModal(); if (m) m.show(); else modalEl.classList.add('show'); 232 } 233 function hideOverlay() { var m = getModal(); if (m) m.hide(); else modalEl.classList.remove('show'); } 234 // Block every close attempt (Escape, programmatic hide) while a selection is forced. 235 modalEl.addEventListener('hide.bs.modal', function (e) { if (window._lpForceSelect) e.preventDefault(); }); 236 237 // âââ Step state machine âââââââââââââââââââââââââââââââââ 238 function showLocationStep(name) { 239 if (state.currentStep && state.currentStep !== name) window._lpPrevStep = state.currentStep; 240 $$('.location-flow-step').forEach(function (s) { 241 if (s.dataset.step === name) s.classList.add('active'); 242 else s.classList.remove('active'); 243 }); 244 state.currentStep = name; 245 246 // Expand the dialog only for the two-panel address form. 247 var dlg = modalEl.querySelector('.modal-dialog'); 248 if (dlg) dlg.classList.toggle('modal-dialog--wide', name === 'addressForm'); 249 250 // Hide the "Already have a Saved Address? Login" CTA on the entry step when the 251 // user is already authenticated â they have direct access to saved addresses. 252 if (name === 'location') { 253 var loginRow = modalEl.querySelector('.alreadyhaveanaccount__cta'); 254 if (loginRow) { 255 var u = userData(); 256 loginRow.style.display = (u.token && u.contactMappingId) ? 'none' : ''; 257 } 258 } 259 260 // Step-specific init 261 if (name === 'savedAddresses') loadSavedAddresses(); 262 if (name === 'enterAddress') initEnterAddressAutocomplete(); 263 if (name === 'addressForm') { 264 // Reset right form to hidden + confirm button to disabled on every 265 // entry; user must re-pick the pin and re-confirm. 266 var rightPanel = $('#addressFormRight'); 267 if (rightPanel) rightPanel.style.display = ''; 268 var confirmCta = modalEl.querySelector('#confirmLocationBtn .btn'); 269 if (confirmCta) confirmCta.disabled = true; 270 initAddressMap(); 271 initHomeLocalityAutocomplete(); 272 } 273 if (name === 'orderType') loadOrderTypes(); 274 if (name === 'redirect') startRedirectCountdown(); 275 if (name === 'location') { 276 var err = $('#error-msg-show'); if (err) err.classList.add('d-none'); 277 } 278 } 279 280 function closeLocationFlow() { if (window._lpForceSelect) return; hideOverlay(); } 281 282 // âââ GPS / current location âââââââââââââââââââââââââââââ 283 function CurrentAddress() { 284 var err = $('#error-msg-show'); 285 if (err) err.classList.add('d-none'); 286 287 if (!navigator.geolocation) { 288 if (err) { err.textContent = 'Geolocation not supported by this browser.'; err.classList.remove('d-none'); } 289 return; 290 } 291 if (!window.isSecureContext) { 292 if (err) { err.textContent = 'Location requires a secure (HTTPS) connection.'; err.classList.remove('d-none'); } 293 return; 294 } 295 296 navigator.geolocation.getCurrentPosition(function (pos) { 297 var lat = pos.coords.latitude, lng = pos.coords.longitude;
298 persistCoords(lat, lng, 'gps'); 299 // CI3 wla_home_20260507.js:935-956 â wipe the prior outlet/address tuple so the 300 // auto-open guard correctly treats this as "in progress" until orderType is picked. 301 localStorage.removeItem('deliveryAddressId'); 302 localStorage.removeItem('selectedDeliveryAddressId'); 303 localStorage.removeItem('selectedAddressId'); 304 localStorage.removeItem('business'); 305 localStorage.removeItem('slug'); 306 localStorage.removeItem('formatted_Address'); 307 // â¦and the LABEL of that address. A GPS point has no address type, so leaving 308 // it behind labelled the detected location with the previous saved address's 309 // type â pick "Home", then Use current location, and the chip still read 310 // "Home" over a completely different place. Every other address-wipe block in 311 // this file (resetLocation, the stale-location sweep) already clears this key 312 // next to `userAddress`; this one omitted it. 313 localStorage.removeItem('userAddressLabel'); 314 // CI3 wla_home_20260507.js:995-1008 â geocode failure stays on location step with 315 // a retry message; never silently advance to orderType without an address. 316 reverseGeocode(lat, lng).then(function (info) { 317 if (!info) { 318 if (err) { err.textContent = 'Unable to fetch your location. Please try again or enter manually.'; err.classList.remove('d-none'); } 319 return; 320 } 321 applyGeocode(info); 322 // Buffer the detected address in memory (applyGeocode no longer persists it). 323 // Nothing touches the outlet's locality/city until the user confirms an 324 // order type (commitAndRedirect). 325 // 'Other' like the locality/pincode paths below: a GPS point has no saved 326 // address type, and leaving it blank made the header inherit the previous 327 // address's label. What matters is that it is always WRITTEN, never inherited. 328 setPendingAddr({ id: '', text: state.address, label: 'Other', locality: state.locality, city: state.city }); 329 showLocationStep('orderType'); 330 }).catch(function () { 331 if (err) { err.textContent = 'Unable to fetch your location. Please try again or enter manually.'; err.classList.remove('d-none'); } 332 }); 333 }, function (e) { 334 var msg = e.code === 1 ? 'Location permission denied. Please enter manually.' : 'Could not get your location. Try entering manually.'; 335 if (err) { err.textContent = msg; err.classList.remove('d-none'); } 336 }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 }); 337 } 338 339 function applyGeocode(info) { 340 if (!info) return; 341 // CI3 wla_home_20260507.js:987-992 â read locality/city (with resp.data.* fallback), 342 // build address as "locality, city" (or whichever single value is present). 343 var loc = info.locality || (info.data && info.data.locality) || ''; 344 var city = info.city || (info.data && info.data.city) || ''; 345 var addr = (loc && city) ? (loc + ', ' + city) : (loc || city || ''); 346 if (addr) state.address = addr; 347 if (loc) state.locality = loc; 348 if (city) state.city = city; 349 if (info.state) state.stateName = info.state; 350 if (info.pincode) state.pinCode = info.pincode; 351 // In-memory ONLY. Do NOT write userAddress/locality/city to localStorage here â those 352 // keys drive the header's "Selected Outlet" pill (locality+city) and "Your Location" 353 // pill (userAddress). Persisting the user's detected location here overwrote the 354 // still-selected outlet's locality/city on the address form / entry GPS paths (business/ 355 // slug stayed pointing at the old outlet), so the outlet pill showed the user's location. 356 // The picked location is buffered via setPendingAddr and committed only on redirect 357 // (flushPendingAddr), after which commitAndRedirect sets locality/city = the OUTLET's. 358 } 359 360 function persistCoords(lat, lng, source) { 361 state.lat = lat; state.lng = lng;
362 localStorage.setItem('userLat', String(lat)); 363 localStorage.setItem('userLng', String(lng)); 364 localStorage.setItem('userLocSource', source); 365 } 366 367 // âââ Upstream API calls (all hit api.uengage.in with api-key) ââââââââ 368 // Mirrors CI3 wla_home_20260507.js:962-1009 â dual auth: token+contactMappingId for 369 // logged-in, sha256(businessId+salt) hashKey for guests. Server: maps_mongodb.php:294-348. 370 // Note: CI3 sent Token in a request header (same-origin, no preflight). CI4 is cross-origin 371 // (wlp-uat â edge-uat), so a custom Token header triggers a CORS preflight the API rejects. 372 // The server accepts `token` as a query param too (maps_mongodb.php:298 reads it before the 373 // header fallback at :306-310), so we keep auth in the URL and avoid preflight on Token. 374 function reverseGeocode(lat, lng) { 375 var ud = userData(); 376 var qs = '?lat=' + lat + '&lng=' + lng + '&businessId=' + pId; 377 if (ud.token && ud.contactMappingId) { 378 qs += '&contactMappingId=' + encodeURIComponent(ud.contactMappingId) 379 + '&token=' + encodeURIComponent(ud.token); 380 } else { 381 var hk = (window.AppConfig && window.AppConfig.geocodeHashKey) || ''; 382 qs += '&hashKey=' + encodeURIComponent(hk); 383 } 384 var url = API_BASE + '/maps/getGeocodeAddress' + qs; 385 return fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' }) 386 .then(function (r) { return r.json(); }) 387 .then(function (d) { return (d && (d.status === 1 || d.status === '1')) ? d : null; }); 388 } 389 390 // Loading row shown in the hybrid results dropdown from the moment a valid (â¥3 char) 391 // query is typed until fetchLocality resolves â so the debounce + network wait reads as 392 // "searching", not "no matches". renderLocalityResults overwrites it with the real list. 393 function renderLocalityLoading(el) { 394 if (!el) return; 395 el.innerHTML = '<li class="lp-search-loading">' 396 + '<span class="lp-inline-spinner" aria-hidden="true"></span>Searching\u2026</li>'; 397 el.style.display = 'block'; 398 } 399 400 var searchTimer = null; 401 function enterAddressSearchLocality(keyword) { 402 // Google flow: places.Autocomplete renders its own .pac-container dropdown when the 403 // SDK is loaded â suppress the AJAX hybrid UI. Falls through if Google auth failed. 404 if (useGoogle && !window._gmapsAuthFailed) return; 405 clearTimeout(searchTimer); 406 var resultsEl = $('#enterAddressLocalityResults'); 407 if (!keyword || keyword.length < 3) { 408 if (resultsEl) { resultsEl.style.display = 'none'; resultsEl.innerHTML = ''; } 409 return; 410 } 411 renderLocalityLoading(resultsEl); 412 searchTimer = setTimeout(function () { 413 fetchLocality(keyword).then(function (items) { renderLocalityResults(resultsEl, items, 'enterAddress'); }); 414 }, 300); 415 } 416 417 var homeSearchTimer = null; 418 function homeSearchLocality(keyword) { 419 if (useGoogle && !window._gmapsAuthFailed) return; 420 clearTimeout(homeSearchTimer); 421 var resultsEl = $('#homeLocalityResults'); 422 if (!keyword || keyword.length < 3) { 423 if (resultsEl) { resultsEl.style.display = 'none'; resultsEl.innerHTML = ''; } 424 return; 425 } 426 renderLocalityLoading(resultsEl); 427 homeSearchTimer = setTimeout(function () { 428 fetchLocality(keyword).then(function (items) { renderLocalityResults(resultsEl, items, 'addressForm'); }); 429 }, 300); 430 } 431 432 // âââ Google Places Autocomplete on entry-step inputs ââââ 433 // Mirrors CI3 footer.php:2155-2210 initEnterAddressAutocomplete. For Google-flow brands 434 // (mapsKey + mapsFlow=1), the entry-step search uses Google Places directly instead of 435 // /client/searchLocality. On place_changed â persist coords/address â orderType step. 436 var _enterAddrAc = null, _homeLocAc = null; 437 function initGoogleAutocomplete(inputId, acRef) { 438 if (!useGoogle || window._gmapsAuthFailed) return; 439 var input = document.getElementById(inputId); 440 if (!input) return; 441 if (acRef.ac) return; // already bound 442 loadGoogleMaps().then(function () { 443 if (!window.google || !google.maps || !google.maps.places) return; 444 acRef.ac = new google.maps.places.Autocomplete(input, { 445 componentRestrictions: { country: 'IN' }, 446 fields: ['address_components', 'geometry.location', 'formatted_address', 'name'] 447 }); 448 acRef.ac.addListener('place_changed', function () { 449 var p = acRef.ac.getPlace(); 450 if (!p || !p.geometry || !p.geometry.location) return; 451 var lat = p.geometry.location.lat(); 452 var lng = p.geometry.location.lng();
453 persistCoords(lat, lng, 'locality'); 454 state.address = p.formatted_address || p.name || ''; 455 localStorage.setItem('searchLocalityInput', JSON.stringify([lat, lng])); 456 // CI3 wla_home_20260507.js:939-955 â locality pick = fresh coords; drop stale tuple. 457 localStorage.removeItem('deliveryAddressId'); 458 localStorage.removeItem('selectedDeliveryAddressId'); 459 localStorage.removeItem('selectedAddressId'); 460 localStorage.removeItem('business'); 461 localStorage.removeItem('slug'); 462 localStorage.removeItem('userAddress'); 463 localStorage.removeItem('userAddressLabel'); 464 // Buffer the picked locality; committed on redirect (flushPendingAddr). 465 // 'Other' â same product decision as the GPS path above; see that comment. 466 setPendingAddr({ id: '', text: state.address, label: 'Other', locality: '', city: '' }); 467 showLocationStep('orderType'); 468 }); 469 }); 470 } 471 function initEnterAddressAutocomplete() { initGoogleAutocomplete('search_locality_new', { get ac() { return _enterAddrAc; }, set ac(v) { _enterAddrAc = v; } }); } 472 function initHomeLocalityAutocomplete() { initGoogleAutocomplete('homeLocalitySearchInput', { get ac() { return _homeLocAc; }, set ac(v) { _homeLocAc = v; } }); } 473 474 // CI3 footer.php:2212-2228 â if Google Maps key is restricted/rejected for the domain, 475 // drop back to the hybrid AJAX path so the user can still search. 476 window.gm_authFailure = function () { 477 window._gmapsAuthFailed = true; 478 _enterAddrAc = null; 479 _homeLocAc = null; 480 }; 481 482 // Hybrid (non-Google) flow: general locality geo-search. Mirrors CI3 483 // ue_silver/home.php:268 â maps_mongodb.php:603 getLocalityByName â a guest, 484 // token-less prefix match over the global maps_places collection that returns 485 // ANY locality + coords. This is NOT /client/searchLocality (which only returns 486 // the brand's own outlets, so typing a real area name gave "No matches"). 487 // Server enforces a 3-char minimum and IP rate limiting. 488 function fetchLocality(keyword) { 489 keyword = (keyword || '').trim(); 490 if (keyword.length < 3) return Promise.resolve([]); 491 var url = API_BASE + '/maps/getLocalityByName' 492 + '?localityName=' + encodeURIComponent(keyword); 493 return fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' }) 494 .then(function (r) { return r.json(); }) 495 .then(function (d) { 496 if (!d || d.status != 1) return []; 497 var items = d.search_results || d.localities || d.data || []; 498 if (!Array.isArray(items)) items = []; 499 return items.map(function (it) { 500 return { 501 localityName: it.localityName || it.locality || it.name || '', 502 cityName: it.cityName || it.city || '', 503 latitude: it.latitude || it.lat || 0, 504 longitude: it.longitude || it.lng || it.long || 0, 505 localityId: it.localityId || it.id || '' 506 }; 507 }).slice(0, 20); 508 }) 509 .catch(function () { return []; }); 510 } 511 512 // Location pin for result rows (stroke=currentColor so CSS colours it). 513 var PIN_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>'; 514 515 function renderLocalityResults(el, items, ctx) { 516 if (!el) return; 517 if (!items.length) { 518 el.innerHTML = '<li class="lp-result-empty">No matches</li>'; 519 el.style.display = 'block'; 520 return; 521 } 522 el.innerHTML = items.map(function (it) { 523 var lat = it.latitude || it.lat || 0; 524 var lng = it.longitude || it.lng || 0; 525 return '<li class="lp-result-row" ' 526 + 'data-lat="' + lat + '" data-lng="' + lng + '" ' 527 + 'data-locality="' + (it.localityName || '').replace(/"/g, '"') + '" ' 528 + 'data-city="' + (it.cityName || '').replace(/"/g, '"') + '">' 529 + '<span class="lp-result-row__pin">' + PIN_ICON + '</span>' 530 + '<span class="lp-result-row__body">' 531 + '<span class="lp-result-row__title">' + (it.localityName || '') + '</span>' 532 + (it.cityName ? '<span class="lp-result-row__sub">' + it.cityName + '</span>' : '') 533 + '</span>' 534 + '</li>'; 535 }).join(''); 536 el.style.display = 'block'; 537 Array.prototype.forEach.call(el.querySelectorAll('.lp-result-row'), function (li) { 538 li.addEventListener('click', function () { selectLocality(li, ctx); }); 539 }); 540 } 541 542 function selectLocality(li, ctx) { 543 var lat = parseFloat(li.dataset.lat); 544 var lng = parseFloat(li.dataset.lng); 545 if (!isFinite(lat) || !isFinite(lng) || (lat === 0 && lng === 0)) return;
546 persistCoords(lat, lng, 'address'); 547 state.locality = li.dataset.locality || ''; 548 state.city = li.dataset.city || ''; 549 // In-memory ONLY â do NOT persist locality/city here. They drive the header's 550 // "Selected Outlet" pill, which must keep showing the already-selected outlet until 551 // the user confirms a new one. addressForm ctx reflects state via applyGeocodeToForm; 552 // entry ctx buffers via setPendingAddr below. commitAndRedirect writes the outlet's 553 // locality/city at confirmation. 554 555 if (ctx === 'addressForm') { 556 applyMapCenter(lat, lng); 557 state.lat = lat; state.lng = lng; 558 // The picked locality is the reference label shown in the right-panel info box. 559 state.address = state.locality + (state.city ? ', ' + state.city : ''); 560 // Funnel through the shared hydrator â same path as a pin-drop / Google 561 // place_changed â so the floating right-side info box (#homeAddrFormInfo) AND the 562 // visible/hidden Locality/City/Lat/Lng fields update and Save unlocks. The hybrid 563 // branch previously hand-set only a subset of inputs and never touched the info box, 564 // so the displayed address didn't change on select. 565 applyGeocodeToForm(lat, lng); 566 var rl = $('#homeLocalityResults'); if (rl) { rl.style.display = 'none'; rl.innerHTML = ''; } 567 // Show the SELECTED locality in the search box (state.address is exactly 568 // that label, built just above). It used to be cleared, leaving nothing on 569 // screen to tell a wrong pick from a right one; the typed fragment is not 570 // the useful reference either, the chosen place is. 571 var si = $('#homeLocalitySearchInput'); if (si && state.address) si.value = state.address; 572 } else { 573 // Entry-step locality pick â mirror CI3 selectHybridLocality (ue_silver/home.php 574 // â showOrderTypeSelector) and the Google-flow place_changed handler: a picked 575 // locality resolves coords and advances straight to the order-type selector 576 // (find nearest outlet), NOT the add-address form. The full delivery address is 577 // collected later. Previously this opened addressForm, diverging from both flows. 578 state.address = state.locality + (state.city ? ', ' + state.city : ''); 579 localStorage.setItem('searchLocalityInput', JSON.stringify([lat, lng])); 580 localStorage.removeItem('deliveryAddressId'); 581 localStorage.removeItem('selectedDeliveryAddressId'); 582 localStorage.removeItem('selectedAddressId'); 583 localStorage.removeItem('business'); 584 localStorage.removeItem('slug'); 585 localStorage.removeItem('userAddress'); 586 localStorage.removeItem('userAddressLabel'); 587 // 'Other' â same product decision as the GPS path; see that comment. 588 setPendingAddr({ id: '', text: state.address, label: 'Other', locality: state.locality, city: state.city }); 589 showLocationStep('orderType'); 590 } 591 } 592 593 // âââ Address form / map âââââââââââââââââââââââââââââââââ 594 // Tracks an in-progress edit from the Manage Addresses page. When set, the 595 // upstream POST carries deliveryAddressId (turning add â update) and the 596 // post-save flow dispatches `lp:address-saved` instead of redirecting. 597 var editingAddressId = 0; 598 var manageMode = false; 599 600 // `manage` = opened standalone from the Manage Addresses page (not the home/product 601 // find-outlet flow). In that mode the form closes fully and save dispatches 602 // `lp:address-saved` instead of advancing to the order-type step. 603 function openHomeAddressForm(manage) { 604 editingAddressId = 0; 605 manageMode = !!manage; 606 // Wipe ALL carried-over state + form fields BEFORE the map inits. `state` is 607 // module-level and the hidden lat/lng + street/email inputs are never cleared by 608 // the browser between opens, so a prior add/edit (or saved-address pick) left its 609 // coords and text in place â and saveHomeAddress read them verbatim, saving the 610 // PREVIOUS location for a brand-new address. Resetting state.lat/lng here also makes 611 // initAddressMap fall back to localStorage (last-known area) instead of re-hydrating 612 // the stale in-memory coords. CI3 normal-flow default copy. 613 state.lat = null; state.lng = null; 614 state.address = ''; state.locality = ''; state.city = ''; 615 state.stateName = ''; state.pinCode = ''; 616 state.savePromise = null; 617 ['#homeAddrLine1', '#homeAddrLine2', '#homeAddrPhone', '#homeAddrEmail', 618 '#homeAddrLat', '#homeAddrLng', 619 '#homeAddrLocality', '#homeAddrCity', '#homeAddrState', '#homeAddrPincode', 620 '#homeAddressSearch'].forEach(function (sel) { 621 var elx = $(sel); if (elx) elx.value = ''; 622 }); 623 // Hide the resolved-address info boxes so a prior address isn't shown until the new 624 // geocode resolves, and default the type radio back to Home (an edit may have moved it). 625 ['#homeAddrMapInfo', '#homeAddrFormInfo'].forEach(function (sel) { 626 var box = $(sel); if (box) box.style.display = 'none'; 627 }); 628 var homeRadio = $('#homeRadio1'); if (homeRadio) homeRadio.checked = true; 629 showLocationStep('addressForm'); 630 showOverlay(); 631 prefillPhoneFromUserdata(); 632 homeSetSaveButtonState(false, 'Select location on map to continue'); 633 // CI3 wla_home_20260507.js:1636-1653 â auto-fetch GPS only when we don't already 634 // have stored coords. Silently no-ops on permission denial; user can still search 635 // or click "Go to current location" manually. 636 var hasStored = localStorage.getItem('userLat') && localStorage.getItem('userLng'); 637 if (!hasStored) homeAddrGoToCurrentLocation(); 638 } 639
640 // CI3 wla_home_20260507.js:1447-1459 â strip optional +91/91, prefill only on a 10-digit 641 // number. Skip if the user already typed something so we don't clobber manual input. 642 function prefillPhoneFromUserdata() { 643 var phoneEl = $('#homeAddrPhone'); 644 if (!phoneEl || phoneEl.value) return; 645 var ud = userData(); 646 var raw = String(ud.mobile || ud.mobileNo || '').replace(/^\+?91/, ''); 647 if (/^\d{10}$/.test(raw)) phoneEl.value = raw; 648 } 649 650 // Wait for the address-form map to finish initializing (Google Maps loads async). 651 // Polls for ~6s so a GPS resolution that beats the map can still center it. 652 function whenMapReady(cb) { 653 if (mapInstance.map) { cb(); return; } 654 var tries = 0; 655 var iv = setInterval(function () { 656 if (mapInstance.map) { clearInterval(iv); cb(); } 657 else if (++tries > 30) { clearInterval(iv); } 658 }, 200); 659 } 660 661 // CI3 wla_home_20260507.js:1678-1690 â gate the Save Address CTA on the map-pin 662 // lifecycle. Same state machine as the checkout add-address modal: 663 // ⢠"Select location on map to continue" â initial / pin not yet placed 664 // ⢠"Save Address" (enabled) â pin placed + locality/city resolved 665 // ⢠"Location not serviceable" â geocode returned status != 1 666 function homeSetSaveButtonState(enabled, message) { 667 var btn = $('#homeAddrSaveBtn'); 668 var err = $('#homeAddrError'); 669 if (btn) { 670 btn.disabled = !enabled; 671 btn.style.opacity = enabled ? '1' : '0.5'; 672 } 673 if (err) { 674 if (enabled || !message) { err.classList.add('d-none'); err.classList.remove('is-loading'); err.textContent = ''; } 675 else { err.classList.remove('d-none', 'is-loading'); err.style.color = ''; err.textContent = message; } 676 } 677 } 678 679 // Show a neutral "Locating your addressâ¦" loader in the address-form footer while the 680 // pin's coords are reverse-geocoded (Google client Geocoder OR hybrid getGeocodeAddress). 681 // Save stays disabled meanwhile; applyGeocodeToForm (success) or the not-serviceable 682 // message (failure) replaces this, so callers must clear it on every resolution path. 683 function homeSetGeocoding() { 684 var btn = $('#homeAddrSaveBtn'); 685 var cta = $('#confirmLocationBtn .btn'); 686 var err = $('#homeAddrError'); 687 if (btn) { btn.disabled = true; btn.style.opacity = '0.5'; } 688 if (cta) cta.disabled = true; 689 if (err) { 690 err.classList.remove('d-none'); 691 err.classList.add('is-loading'); 692 err.style.color = ''; 693 err.innerHTML = '<span class="lp-inline-spinner" aria-hidden="true"></span>Locating your address\u2026'; 694 } 695 } 696 697 // Re-render the map after a container resize WITHOUT the resize's own moveend/idle 698 // re-geocoding. invalidateSize preserves the centre, but getCenter() jitters ~1e-5 after a 699 // resize â past the dedupe â so the move handler would fire a second, redundant geocode of 700 // the same point (the "2 calls / fluctuating between 2 addresses" bug). The flag makes the 701 // move handlers skip that one programmatic move. 702 function resizeHomeMap() { 703 if (!mapInstance.map) return; 704 mapInstance.suppressGeo = true; 705 if (mapInstance.kind === 'leaflet' && mapInstance.map.invalidateSize) mapInstance.map.invalidateSize(); // default pan:true PRESERVES centre 706 else if (mapInstance.kind === 'google' && window.google && window.google.maps && window.google.maps.event) window.google.maps.event.trigger(mapInstance.map, 'resize'); 707 setTimeout(function () { mapInstance.suppressGeo = false; }, 300); 708 } 709 710 // Shared form-field hydration after a geocode resolves. Used by both onPinChanged 711 // (map drag) and homeAddrGoToCurrentLocation (button click + auto-GPS on open) so 712 // locality/city/state/pincode land in the hidden inputs and the confirm CTA activates. 713 // (Locality/City have no visible field â they show in the green card â but their hidden 714 // inputs #homeAddrLocality / #homeAddrCity below still feed the save payload.) 715 function applyGeocodeToForm(lat, lng) { 716 $('#homeAddrLat') && ($('#homeAddrLat').value = lat); 717 $('#homeAddrLng') && ($('#homeAddrLng').value = lng); 718 $('#homeAddrCity') && ($('#homeAddrCity').value = state.city || ''); 719 $('#homeAddrState') && ($('#homeAddrState').value = state.stateName || ''); 720 $('#homeAddrPincode') && ($('#homeAddrPincode').value = state.pinCode || ''); 721 $('#homeAddrLocality') && ($('#homeAddrLocality').value = state.locality || ''); 722 // Reference shows on both screens: the map step (#homeAddrMapInfo, above 723 // Confirm Location) and the form step (#homeAddrFormInfo, under the header). 724 var loc = state.locality || ''; 725 var addr = state.address || ''; 726 // Green card + map card show "locality, city" â but only append city when the locality 727 // doesn't already contain it (avoids "Kolkata, Kolkata" / "Panchkula, Panchkula"). 728 var cityName = state.city || ''; 729 var locWithCity = (cityName && loc.toLowerCase().indexOf(cityName.toLowerCase()) === -1) 730 ? (loc ? (loc + ', ' + cityName) : cityName) : loc; 731 var info1 = $('#homeAddrMapLocality'); if (info1) info1.textContent = locWithCity; 732 var info2 = $('#homeAddrMapFullAddress');
732 if (info2) info2.textContent = addr; 733 var info3 = $('#homeAddrFormLocality'); if (info3) info3.textContent = locWithCity; 734 var info4 = $('#homeAddrFormFullAddress'); if (info4) info4.textContent = addr; 735 var box = $('#homeAddrMapInfo'); if (box) box.style.display = 'block'; 736 var box2 = $('#homeAddrFormInfo'); if (box2) box2.style.display = 'block'; 737 // Showing the below-map card shrinks the map (flex-fill layout) â re-render so it 738 // doesn't show gray tiles, WITHOUT the resize's moveend re-geocoding the same point. 739 setTimeout(resizeHomeMap, 80); 740 var cta = $('#confirmLocationBtn .btn'); if (cta) cta.disabled = false; 741 if (state.locality && state.city) homeSetSaveButtonState(true, 'Save Address'); 742 else homeSetSaveButtonState(false, 'Select location on map to continue'); 743 } 744 745 // CI3 wla_home_20260507.js:1714-1724 â parse a Google Places/Geocoder 746 // address_components array into state. City = the 'locality' type; Locality = 747 // 'sublocality' (falls back to city); plus admin_area_1 â state, postal_code â pincode. 748 function applyAddressComponents(components) { 749 if (!components || !components.length) return; 750 var city = '', stateName = '', pincode = '', locality = ''; 751 components.forEach(function (c) { 752 var t = c.types || []; 753 if (t.indexOf('locality') > -1) city = c.long_name; 754 if (t.indexOf('administrative_area_level_1') > -1) stateName = c.long_name; 755 if (t.indexOf('postal_code') > -1) pincode = c.long_name; 756 if (t.indexOf('sublocality_level_1') > -1 || t.indexOf('sublocality') > -1) locality = c.long_name; 757 }); 758 state.city = city; 759 state.locality = locality || city; 760 if (stateName) state.stateName = stateName; 761 if (pincode) state.pinCode = pincode; 762 } 763 764 // CI3 wla_home_20260507.js:1703-1738 (updateHomeAddressFromLatLng) â the single 765 // reverse-geocode entry point for the address form, shared by search, pin drag, and 766 // the GPS button. Google-flow brands use the client-side Geocoder and add the address 767 // UNCONDITIONALLY (no serviceability call); serviceability is decided later at checkout 768 // when the nearest open outlet is resolved. Non-Google (Leaflet) brands have no client 769 // geocoder, so they fall back to the AJAX getGeocodeAddress â their only option. 770 function hydrateFormFromLatLng(lat, lng) { 771 state.lat = lat; state.lng = lng; 772 homeSetGeocoding(); 773 if (useGoogle && !window._gmapsAuthFailed && window.google && google.maps && google.maps.Geocoder) { 774 var geocoder = new google.maps.Geocoder(); 775 geocoder.geocode({ location: { lat: lat, lng: lng } }, function (results, status) { 776 if (status !== 'OK' || !results[0]) { homeSetSaveButtonState(false, 'Location not serviceable'); return; } 777 applyAddressComponents(results[0].address_components); 778 state.address = results[0].formatted_address || state.address; 779 if (state.address) localStorage.setItem('userAddress', state.address); 780 // The search box is LEFT ALONE. It used to be cleared here so it would 781 // not sit prefilled with a resolved address â but this runs on every 782 // geocode, including the one fired when the form opens, so anyone who 783 // started typing while the map was still loading had their text wiped 784 // out from under them. It is the user's input, not a label: only they 785 // clear it. 786 applyGeocodeToForm(lat, lng); 787 }); 788 return; 789 } 790 reverseGeocode(lat, lng).then(function (info) { 791 if (!info) { homeSetSaveButtonState(false, 'Location not serviceable'); return; } 792 applyGeocode(info); 793 applyGeocodeToForm(lat, lng); 794 }); 795 } 796 797 function openEditAddressForm(addr) { 798 if (!addr || typeof addr !== 'object') return; 799 editingAddressId = parseInt(addr.id || addr.deliveryAddressId || 0, 10) || 0; 800 manageMode = true; 801 802 // Open the modal first so the form fields exist in the DOM 803 showLocationStep('addressForm'); 804 showOverlay(); 805 806 // Pre-fill â fields populated by initAddressMap may overwrite some of these, 807 // so apply on the next tick to win over the initial centering. 808 setTimeout(function () { 809 var setVal = function (sel, v) { var el = $(sel); if (el) el.value = (v == null ? '' : v); }; 810 setVal('#homeAddrLine1', addr.addressLine1 || addr.line1 || ''); 811 setVal('#homeAddrLine2', addr.addressLine2 || addr.line2 || ''); 812 setVal('#homeAddrPhone', addr.mobile || addr.phone || ''); 813 setVal('#homeAddrEmail', addr.email || ''); 814 // Seed in-memory state from the saved address so the shared hydrator below 815 // can fill the form from it. state.address is the reference label shown in the 816 // under-map / under-header info boxes. 817 state.locality = addr.locality || ''; 818 state.city = addr.city || ''; 819 state.stateName = addr.state || addr.stateName || ''; 820 state.pinCode = addr.pinCode || addr.pincode || ''; 821 // Info box shows locality (bold) + this sub-line. Keep it to city only â not the 822 // full street address. 823 state.address = addr.city || ''; 824 var lat = parseFloat(addr.latitude || addr.lat || 0); 825 var lng = parseFloat(addr.longitude || addr.lng || 0); 826 if (lat && lng) { state.lat = lat; state.lng = lng; applyMapCenter(lat, lng); } 827 // Reuse the shared hydrator instead of hand-filling fields. It populates the 828 // right-panel inputs (visible + hidden Locality/City/State/Pincode/Lat/Lng) AND 829 // the under-map / under-header info boxes (#homeAddrMapInfo / #homeAddrFormInfo) 830 // â which openEditAddressForm previously never touched, so the locality + full 831 // address stayed hidden below the map on edit â and unlocks Save. 832 applyGeocodeToForm(state.lat || lat || 0, state.lng || lng || 0); 833 // Address type radio (Home/Office/Hotel/Other) â resolve the same way the list does 834 // (handles numeric addressType/name + custom addressValue labels), then fall back to 835 // 'Other' if the resolved label isn't one of the four standard radios. 836 var typeName = resolveAddressLabel(addr); 837 if (['Home', 'Office', 'Hotel', 'Other'].indexOf(typeName) === -1) typeName = 'Other'; 838 var typeRadio = modalEl.querySelector('input[name="homeAddrType"][value="' + typeName + '"]'); 839 if (typeRadio) typeRadio.checked = true; 840 }, 0); 841 } 842 843 // Geolocation failures show on the MAP step (#homeLocateError), not the form banner â 844 // on mobile the "Use my location" button lives on the map step, so the message must too. 845 function homeShowError(msg) { 846 var box = $('#homeLocateError'); 847 if (!box) return; 848 box.hidden = false;
849 box.textContent = msg; 850 } 851 852 // showError: true when triggered by the manual "Use my location" button â surfaces a 853 // reason on failure (denied permission / insecure-context block) inline instead of 854 // silently no-op'ing. The auto-call on form open (openHomeAddressForm) passes nothing â quiet. 855 function homeAddrGoToCurrentLocation(showError) { 856 var lerr = $('#homeLocateError'); if (lerr) lerr.hidden = true; // clear any prior failure 857 if (!navigator.geolocation) { 858 if (showError) homeShowError('Location services are not available in this browser.'); 859 return; 860 } 861 navigator.geolocation.getCurrentPosition(function (pos) { 862 var lat = pos.coords.latitude, lng = pos.coords.longitude; 863 persistCoords(lat, lng, 'gps'); 864 // When called from openHomeAddressForm the map is still loading, so defer the 865 // center + reverse-geocode until it's ready. For a manual click the map is 866 // already up and whenMapReady invokes the callback synchronously. 867 // CI3 wla_home_20260507.js:1639-1650 â auto-GPS on the address form hydrates via 868 // the same path as search/drag (Google client geocoder, no add-time gate). 869 whenMapReady(function () { 870 applyMapCenter(lat, lng); 871 hydrateFormFromLatLng(lat, lng); 872 }); 873 }, function () { 874 if (showError) homeShowError('Unable to get your location. Please allow location access â this needs a secure (HTTPS) page.'); 875 }, { enableHighAccuracy: true, timeout: 10000 }); 876 } 877 878 function confirmLocationAndShowForm() { 879 // Mobile two-step flow: hide map, reveal the form panel. Desktop ignores 880 // the class (panels are side-by-side via CSS). 881 var container = modalEl.querySelector('.homeAddressFormModal .addadress-starting'); 882 if (container) container.classList.add('is-form-visible'); 883 } 884 885 function backToLocationMap() { 886 var container = modalEl.querySelector('.homeAddressFormModal .addadress-starting'); 887 if (container) container.classList.remove('is-form-visible'); 888 } 889 890 // Green card's "Change on map" â mirrors checkout's addressForm.js: on mobile swap 891 // back to the map screen, then focus + flash the map search field so the user can re-pick. 892 function homeChangeOnMap() { 893 backToLocationMap(); 894 var input = $('#homeAddressSearch') || $('#homeLocalitySearchInput'); 895 var wrap = input && input.closest('.map-search'); 896 setTimeout(function () { 897 if (input) input.focus(); 898 if (!wrap) return; 899 wrap.classList.add('map-search--flash'); 900 setTimeout(function () { wrap.classList.remove('map-search--flash'); }, 1200); 901 }, 60); 902 } 903 904 // Close (X) handler for the address-form step. On the Manage Addresses page the form is 905 // opened standalone (manageMode), so closing fully dismisses the modal â it must NOT fall 906 // back into the saved-addresses / find-outlet flow, which is restricted to home + product 907 // pages. In the home/product flow the form is reached from savedAddresses, so it returns there. 908 function closeAddressForm() { 909 if (manageMode) closeLocationFlow(); 910 else showLocationStep('savedAddresses'); 911 } 912 913 function saveHomeAddress() { 914 var ud = userData(); 915 var errEl = $('#homeAddrError'); 916 if (errEl) errEl.classList.add('d-none'); 917 918 var line1 = ($('#homeAddrLine1') || {}).value || ''; 919 var line2 = ($('#homeAddrLine2') || {}).value || ''; 920 var phone = ($('#homeAddrPhone') || {}).value || ''; 921 var email = ($('#homeAddrEmail') || {}).value || ''; 922 var typeEl = modalEl.querySelector('input[name="homeAddrType"]:checked'); 923 var typeVal = typeEl ? typeEl.value : 'Home'; 924 var lat = parseFloat(($('#homeAddrLat') || {}).value || '') || state.lat || 0; 925 var lng = parseFloat(($('#homeAddrLng') || {}).value || '') || state.lng || 0; 926 927 line1 = line1.trim(); 928 phone = phone.trim(); 929 if (line1.length < 3) { 930 if (errEl) { errEl.textContent = 'Please enter a valid house / flat / block.'; errEl.classList.remove('d-none', 'is-loading'); } 931 return; 932 } 933 if (!/^[0-9]{10}$/.test(phone)) { 934 if (errEl) { errEl.textContent = 'Please enter a valid 10-digit phone number.'; errEl.classList.remove('d-none', 'is-loading'); } 935 return; 936 } 937 if (!lat || !lng) { 938 if (errEl) { errEl.textContent = 'Please pick a location on the map.'; errEl.classList.remove('d-none', 'is-loading'); } 939 return; 940 } 941 942 var locality = state.locality || (($('#homeAddrLocality') || {}).value || ''); 943 var city = state.city || (($('#homeAddrCity') || {}).value || ''); 944 if (!locality.trim() || !city.trim()) { 945 if (errEl) { errEl.textContent = 'Please pick a serviceable location on the map.'; errEl.classList.remove('d-none', 'is-loading'); } 946 return; 947 }
948 var fullAddr = [line1, line2, locality, city].filter(Boolean).join(', '); 949 // Explicit user confirmation â persist coords now (needed for the nearest-outlet 950 // lookup). The address identity is held in memory until redirect (flushPendingAddr), 951 // so adding an address that turns out non-serviceable never leaks onto checkout. 952 persistCoords(lat, lng, 'address'); 953 if (!manageMode) { 954 setPendingAddr({ id: editingAddressId || '', text: fullAddr, label: typeVal, locality: locality, city: city }); 955 } 956 957 // Save to upstream only for logged-in users (CI3 contract). 958 // CI3 reuses /client/addDeliveryAddress for BOTH add and update â passing 959 // deliveryAddressId in the body switches it to update mode. 960 if (ud.token && ud.contactMappingId) { 961 var url = API_BASE + '/client/addDeliveryAddress/' + pId + '/' + encodeURIComponent(ud.contactMappingId) + '/' + encodeURIComponent(ud.token); 962 var form = new URLSearchParams(); 963 form.set('addressLine1', line1); 964 form.set('addressLine2', line2); 965 form.set('locality', locality); 966 form.set('city', city); 967 form.set('state', state.stateName || ''); 968 form.set('pinCode', state.pinCode || ''); 969 form.set('mobile', phone); 970 form.set('email', email); 971 form.set('name', typeVal); 972 form.set('type', typeVal); 973 form.set('latitude', String(lat)); 974 form.set('longitude', String(lng)); 975 if (ud.contactId) form.set('contactId', String(ud.contactId)); 976 if (editingAddressId) form.set('deliveryAddressId', String(editingAddressId)); 977 var wasManage = manageMode; 978 // Snapshot the edit-id: this callback is async and editingAddressId is reset 979 // synchronously below, so read the captured value, not the live module var. 980 var wasEditId = editingAddressId; 981 state.savePromise = fetch(url, { 982 method: 'POST', 983 headers: apiHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }), 984 body: form.toString(), 985 credentials: 'omit' 986 }).then(function (r) { return r.json(); }) 987 .then(function (d) { 988 // Upstream id shape varies by path â accept rows.deliveryAddressId / rows.id / 989 // top-level deliveryAddressId / data.deliveryAddressId. Falls back to the edited 990 // id on an update. 991 var id = (d && ((d.rows && (d.rows.deliveryAddressId || d.rows.id)) 992 || d.deliveryAddressId 993 || (d.data && d.data.deliveryAddressId))) || wasEditId; 994 // Attach the saved id to the buffered selection â still NOT written to the 995 // checkout keys; that happens on redirect via flushPendingAddr. 996 if (id && !wasManage && state.pendingAddr) state.pendingAddr.id = id; 997 if (wasManage) { 998 window.dispatchEvent(new CustomEvent('lp:address-saved')); 999 } 1000 }).catch(function () {}); 1001 } 1002 1003 // Reset the edit-id after every save (not just the manage branch) so a subsequent 1004 // "Add new address" can't inherit the id of the address just edited. 1005 editingAddressId = 0; 1006 if (manageMode) { 1007 // Manage Addresses flow â close the modal, don't redirect to order type. 1008 manageMode = false; 1009 hideOverlay(); 1010 } else { 1011 showLocationStep('orderType'); 1012 } 1013 } 1014 1015 // âââ Saved addresses ââââââââââââââââââââââââââââââââââââ 1016 function openLoginOrSavedAddresses() { 1017 var ud = userData(); 1018 if (!ud.token || !ud.contactMappingId) { 1019 // Close the location modal first so the login modal isn't stacked on top of it. 1020 // Hook _onLoginSuccess so we re-open at savedAddresses once auth completes. 1021 hideOverlay(); 1022 window._onLoginSuccess = function () { 1023 window._onLoginSuccess = null; 1024 showLocationStep('savedAddresses'); 1025 showOverlay(); 1026 }; 1027 if (typeof window.showLogin === 'function') { 1028 // Small delay so Bootstrap can finish hiding the location modal before showing the next. 1029 setTimeout(function () { window.showLogin(); }, 200); 1030 } 1031 return; 1032 } 1033 showLocationStep('savedAddresses'); 1034 } 1035 1036 // Numeric type-id â label. Mirrors checkout.js:2009 ADDR_TYPE so both screens agree. 1037 var TYPE_LABELS = { 1: 'Home', 2: 'Office', 3: 'Hotel', 4: 'Other' }; 1038 // String type-name â canonical label (case-insensitive). 'work' is checkout's alias for office.
1039 var TYPE_NAMES = { home: 'Home', office: 'Office', work: 'Office', hotel: 'Hotel', other: 'Other', others: 'Other' }; 1040 var TYPE_ICONS = { 1041 Home: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-7 9 7v9a1 1 0 01-1 1h-5v-6h-6v6H4a1 1 0 01-1-1v-9z"/></svg>', 1042 Office: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="13" rx="1.5"/><path d="M8 7V5a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M3 13h18"/></svg>', 1043 Hotel: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 18v-6h14a4 4 0 014 4v2"/><path d="M2 18h20"/><path d="M2 18v3"/><path d="M22 18v3"/><circle cx="7" cy="11" r="2.2"/></svg>', 1044 Other: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>' 1045 }; 1046 var CHEV_RIGHT = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M9 18l6-6-6-6"/></svg>'; 1047 1048 function resolveAddressLabel(a) { 1049 // The address type lives in `name` â a numeric id ("1".."4"), a string label ("Office"), 1050 // or null. checkout.js:2106 reads the SAME field via addrTypeLabel(addr.name); this list 1051 // ignored `name`, so every row fell through to 'Home'. `addressType`/`addressValue` are 1052 // noisy in production (zero-padded "00000000000", literal "0"), so they're a last resort. 1053 var candidates = [a.name, a.addressType, a.addressValue, a.type]; 1054 for (var i = 0; i < candidates.length; i++) { 1055 var v = String(candidates[i] == null ? '' : candidates[i]).trim(); 1056 if (!v) continue; 1057 if (/^\d+$/.test(v)) { // numeric id (incl. zero-padded) â 0 = unset 1058 var n = parseInt(v, 10); 1059 if (n !== 0 && TYPE_LABELS[n]) return TYPE_LABELS[n]; 1060 continue; 1061 } 1062 return TYPE_NAMES[v.toLowerCase()] || v; // known label, else a genuine custom label 1063 } 1064 return 'Home'; 1065 } 1066 1067 function iconFor(label) { 1068 return TYPE_ICONS[label] || TYPE_ICONS.Other; 1069 } 1070 1071 function escAttr(s) { return String(s == null ? '' : s).replace(/"/g, '"'); } 1072 function escHtml(s) { 1073 return String(s == null ? '' : s) 1074 .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); 1075 } 1076 1077 function cleanFieldList(arr) { 1078 // Drop empty strings, "0" placeholders, and any numeric-zero stand-ins from the joined address. 1079 return arr.filter(function (v) { 1080 if (v === null || v === undefined) return false; 1081 var s = String(v).trim(); 1082 if (!s) return false; 1083 if (s === '0' || s === '0.0') return false; 1084 return true; 1085 }); 1086 } 1087 1088 // Saved-address pagination: show the first 3, then reveal 4 more per 1089 // "View More" click (CI3 home-flow chunking behaviour). 1090 var SAVED_ADDR_INITIAL = 3; 1091 var SAVED_ADDR_CHUNK = 4; 1092 1093 function savedAddrRowHtml(a) { 1094 var label = resolveAddressLabel(a); 1095 var full = cleanFieldList([a.addressLine1, a.addressLine2, a.locality, a.city, a.pinCode]).join(', '); 1096 // getDeliveryAddress returns coords as latitude/longitude OR lat/lng depending on the 1097 // upstream path â accept both (manageAddresses.js normalizes the same way). Without the 1098 // lat/lng fallback, coords-bearing rows render empty â onPickSavedAddress diverts to the 1099 // address step and getNearestOpenOutlet never fires. 1100 var aLat = a.latitude || a.lat || ''; 1101 var aLng = a.longitude || a.lng || ''; 1102 return '<div class="saved-addr-row" ' 1103 + 'data-id="' + escAttr(a.deliveryAddressId || a.id || '') + '" ' 1104 + 'data-lat="' + escAttr(aLat) + '" ' 1105 + 'data-lng="' + escAttr(aLng) + '" ' 1106 + 'data-locality="' + escAttr(a.locality || '') + '" ' 1107 + 'data-city="' + escAttr(a.city || '') + '" ' 1108 + 'data-address="' + escAttr(full) + '" ' 1109 + 'data-label="' + escAttr(label) + '">' 1110 + '<span class="saved-addr-row__icon">' + iconFor(label) + '</span>' 1111 + '<div class="saved-addr-row__main">' 1112 + '<span class="saved-addr-row__label">' + escHtml(label) + '</span>' 1113 + '<span class="saved-addr-row__text" title="' + escAttr(full) + '">' + escHtml(full) + '</span>' 1114 + '</div>' 1115 + '<span class="saved-addr-row__chev">' + CHEV_RIGHT + '</span>' 1116 + '</div>'; 1117 } 1118 1119 // Bind the click handler to any saved-addr rows that aren't wired up yet. 1120 function bindSavedAddrRows(listEl) { 1121 Array.prototype.forEach.call(listEl.querySelectorAll('.saved-addr-row'), function (row) { 1122 if (row.dataset.bound === '1') return; 1123 row.dataset.bound = '1'; 1124 row.addEventListener('click', function () { onPickSavedAddress(row); }); 1125 }); 1126 } 1127 1128 // Render `shown` of `rows` plus a toggle link: "View More" reveals the 1129 // next chunk, "View Less" (once all are shown) collapses back to the 1130 // initial count.
1131 function renderSavedAddresses(listEl, rows, shown) { 1132 listEl.innerHTML = rows.slice(0, shown).map(savedAddrRowHtml).join(''); 1133 bindSavedAddrRows(listEl); 1134 if (rows.length <= SAVED_ADDR_INITIAL) return; 1135 1136 var toggle = document.createElement('div'); 1137 toggle.className = 'saved-addr-more'; 1138 toggle.id = 'savedAddrViewMore'; 1139 var allShown = shown >= rows.length; 1140 toggle.textContent = allShown ? 'View Less' : 'View More'; 1141 toggle.addEventListener('click', function () { 1142 var next = allShown 1143 ? SAVED_ADDR_INITIAL 1144 : Math.min(shown + SAVED_ADDR_CHUNK, rows.length); 1145 renderSavedAddresses(listEl, rows, next); 1146 }); 1147 listEl.appendChild(toggle); 1148 } 1149 1150 function loadSavedAddresses() { 1151 var ud = userData(); 1152 // Personalise the "Welcome Back" greeting with the user's first name (client-side 1153 // data, so it can't be rendered by PHP). Falls back to a plain "Welcome Back". 1154 var nameEl = $('#lpWelcomeName'); 1155 if (nameEl) { 1156 var first = String(ud.name || '').trim().split(/\s+/)[0] || ''; 1157 // Suppress empty/null and the generic "user" placeholder â greet by name only 1158 // when it's a real given name. 1159 if (first.toLowerCase() === 'user') first = ''; 1160 nameEl.textContent = first ? (', ' + first) : ''; 1161 } 1162 var listEl = $('#savedaddress_list'); 1163 if (!listEl) return; 1164 if (!ud.token || !ud.contactMappingId) { 1165 listEl.innerHTML = '<div class="saved-addr-empty">Please log in to see saved addresses.</div>'; 1166 return; 1167 } 1168 listEl.innerHTML = '<div class="saved-addr-empty">Loading...</div>'; 1169 var url = API_BASE + '/client/getDeliveryAddress/' + pId + '/' + encodeURIComponent(ud.contactMappingId) + '/' + encodeURIComponent(ud.token); 1170 fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' }) 1171 .then(function (r) { return r.json(); }) 1172 .then(function (d) { 1173 var rows = (d && (d.data || d.rows)) || []; 1174 rows = rows.filter(function (a) { 1175 return cleanFieldList([a.addressLine1, a.addressLine2, a.locality, a.city, a.pinCode]).length > 0; 1176 }); 1177 if (!rows.length) { 1178 listEl.innerHTML = '<div class="saved-addr-empty">No saved addresses found</div>'; 1179 return; 1180 } 1181 // Show the first SAVED_ADDR_INITIAL rows, then reveal 1182 // SAVED_ADDR_CHUNK more per "View More" click. Once everything 1183 // is visible a "View Less" link collapses back (CI3 chunking). 1184 renderSavedAddresses(listEl, rows, Math.min(SAVED_ADDR_INITIAL, rows.length)); 1185 }) 1186 .catch(function () { 1187 listEl.innerHTML = '<div class="saved-addr-empty">Could not load addresses.</div>'; 1188 }); 1189 } 1190 1191 function onPickSavedAddress(row) { 1192 var lat = parseFloat(row.dataset.lat); 1193 var lng = parseFloat(row.dataset.lng); 1194 var id = row.dataset.id; 1195 // Hold the selection in memory only â committed on redirect (flushPendingAddr). 1196 setPendingAddr({ 1197 id: id, 1198 text: row.dataset.address || '', 1199 label: row.dataset.label || 'Home', 1200 locality: row.dataset.locality || '', 1201 city: row.dataset.city || '' 1202 }); 1203 1204 if (isFinite(lat) && isFinite(lng) && lat !== 0 && lng !== 0) { 1205 persistCoords(lat, lng, 'address'); 1206 state.locality = row.dataset.locality || ''; 1207 state.city = row.dataset.city || ''; 1208 showLocationStep('orderType'); 1209 } else { 1210 showLocationStep('enterAddress'); 1211 } 1212 } 1213 1214 // âââ Order type / nearest store âââââââââââââââââââââââââ 1215 function loadOrderTypes() { 1216 var lat = state.lat || parseFloat(localStorage.getItem('userLat') || '0'); 1217 var lng = state.lng || parseFloat(localStorage.getItem('userLng') || '0'); 1218 if (!isFinite(lat) || !isFinite(lng) || (lat === 0 && lng === 0)) { 1219 // No coords â but we may already know WHICH outlet this is, from the 1220 // menu page the visitor is coming off (_seedCurrentOutlet). The 1221 // coordinate lookup below exists to answer "which outlet serves you"; 1222 // that question is already answered, so ask the one that is actually 1223 // open instead: how do you want it. This is the menu page's own 1224 // switcher behaviour, reached from every other page. 1225 var card = storedOutletCard(); 1226 if (card) { 1227 window._lpSelectedStore = card; 1228 var cAddrEl = $('#orderTypeModal_address'); 1229 if (cAddrEl) cAddrEl.textContent = [card.locality, card.city].filter(
1229Boolean).join(', '); 1230 renderOrderTypes(card); 1231 return; 1232 } 1233 // Nothing known at all (e.g. an outlet picked on the store-locator 1234 // before this record existed). Send logged-in users to their saved 1235 // addresses so they can re-anchor a location with coords; only 1236 // anonymous users go to step 1. 1237 var ud = userData(); 1238 showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location'); 1239 return; 1240 } 1241 1242 var addrEl = $('#orderTypeModal_address'); 1243 if (addrEl) { 1244 // Fall back to stored locality/city (in-memory state is empty after a reload for a 1245 // store-locator pick, which has no userAddress) so this never shows a bare ", ". 1246 var loc = state.locality || localStorage.getItem('locality') || ''; 1247 var cty = state.city || localStorage.getItem('city') || ''; 1248 // Prefer the in-memory buffer (not yet committed to localStorage) for display. 1249 var pend = state.pendingAddr && state.pendingAddr.text; 1250 addrEl.textContent = pend || localStorage.getItem('userAddress') || [loc, cty].filter(Boolean).join(', '); 1251 } 1252 1253 // api.uengage.in/client/getNearestOpenOutlet returns a single { address: {...} }, 1254 // so wrap into an array for the downstream picker logic. 1255 var url = API_BASE + '/client/getNearestOpenOutlet' 1256 + '?parentBusinessId=' + pId 1257 + '&latitude=' + lat 1258 + '&longitude=' + lng 1259 + (window._lpPrebooking ? '&prebooking=1' : ''); 1260 fetch(url, { method: 'GET', headers: apiHeaders(), credentials: 'omit' }) 1261 .then(function (r) { return r.json(); }) 1262 .then(function (d) { 1263 var rows = []; 1264 if (d && Number(d.status) === 1 && d.address) { 1265 rows = Array.isArray(d.address) ? d.address : [d.address]; 1266 } else if (d && (d.data || d.rows)) { 1267 rows = d.data || d.rows; 1268 } 1269 if (!rows.length) { 1270 showLocationStep('notServiceable'); 1271 return; 1272 } 1273 state.nearest = rows; 1274 var store = pickNearestOpen(rows); 1275 window._lpSelectedStore = store; 1276 1277 // Order type already chosen (e.g. home-page Action Center card) â skip the 1278 // visual orderType step and go straight to the redirect, same as if the user 1279 // had tapped that type's button here. Falls through to the normal picker if 1280 // the nearest outlet doesn't actually support the pre-chosen type. 1281 var pendingType = window._lpPendingOrderType; 1282 window._lpPendingOrderType = null; 1283 if (pendingType) { 1284 var flagKey = FLAG_BY_TYPE[pendingType]; 1285 var supported = flagKey ? Number(store[flagKey]) === 1 : false; 1286 if (Number(pendingType) === 1) { 1287 supported = supported && Number(store.onlineDeliveryOn !== undefined ? store.onlineDeliveryOn : 1) === 1; 1288 } 1289 if (supported) { 1290 selectOrderTypeAndRedirect(pendingType); 1291 return; 1292 } 1293 } 1294 renderOrderTypes(store); 1295 }) 1296 .catch(function () { showLocationStep('notServiceable'); }); 1297 } 1298 1299 function pickNearestOpen(stores) { 1300 for (var i = 0; i < stores.length; i++) { 1301 if (stores[i].isOpen === true || Number(stores[i].isOpen) === 1) return stores[i]; 1302 } 1303 return stores[0]; 1304 } 1305 1306 // The outlet the visitor is currently browsing, recorded by _seedCurrentOutlet 1307 // below. Read back only while it still describes the stored outlet: `business` 1308 // is the authority, and a journey pick overwrites that without touching this. 1309 var OUTLET_CARD_KEY = 'v4_current_outlet'; 1310 function storedOutletCard() { 1311 var business = localStorage.getItem('business'); 1312 if (!business || business === 'null') return null; 1313 try { 1314 var card = JSON.parse(localStorage.getItem(OUTLET_CARD_KEY) || 'null'); 1315 return (card && card.id && String(card.id) === String(business)) ? card : null; 1316 } catch (e) { return null; } 1317 } 1318 1319 var FLAG_BY_TYPE = { 1320 1: 'onlineOrdersDelivery', 1321 2: 'onlineOrdersSelfPickup', 1322 3: 'dineInOrders', 1323 4: 'inCarOrders', 1324 5: 'reservationAllowed', 1325 6: 'uengage_pay' 1326 }; 1327 1328 function renderOrderTypes(store) { 1329 var enabledIds = [];
1330 Object.keys(FLAG_BY_TYPE).forEach(function (id) { 1331 var on = Number(store[FLAG_BY_TYPE[id]]) === 1; 1332 if (Number(id) === 1) on = on && Number(store.onlineDeliveryOn !== undefined ? store.onlineDeliveryOn : 1) === 1; 1333 if (on) enabledIds.push(id); 1334 var btn = document.getElementById('orderTypeBtn_' + id); 1335 if (btn) btn.style.display = on ? '' : 'none'; 1336 }); 1337 var grid = document.getElementById('orderTypeButtonsContainer'); 1338 var single = enabledIds.length === 1; 1339 if (grid) grid.classList.toggle('order-type-grid--single', single); 1340 1341 // No order type at all (every flag off): all cards are hidden, so the grid would 1342 // sit empty under a "how would you like your order?" prompt. Swap in a message 1343 // instead â the "Change" control above it is the way out. 1344 var none = enabledIds.length === 0; 1345 var empty = document.getElementById('orderTypeEmpty'); 1346 if (empty) empty.hidden = !none; 1347 if (grid) grid.hidden = none; 1348 1349 // With a single order type there is no choice to make, so the "Select an order 1350 // typeâ¦" framing reads oddly. Reframe the heading as a continue prompt; the lone 1351 // card is styled as a full-width primary CTA (see .order-type-grid--single CSS). 1352 var heading = document.getElementById('orderTypeHeading'); 1353 if (heading) {
1354 // With no order type the highlighted band below states the situation, so the 1355 // prompt is dropped rather than repeated. style.display, not [hidden] â the 1356 // heading's own rule carries !important and would win over the attribute. 1357 heading.style.display = none ? 'none' : ''; 1358 if (!none) { 1359 heading.textContent = single 1360 ? "Great, you're covered! Continue to place your order" 1361 : "Great, you're covered! How would you like your order?"; 1362 } 1363 } 1364 1365 // Heal a stale stored orderType (e.g. delivery='1' left over from a previous 1366 // session on a brand where delivery is now disabled). The header info bar renders 1367 // this value verbatim â without correcting it, it shows "Delivery" for a pickup-only 1368 // outlet. Reset to the first enabled type and refresh the bar so the label matches 1369 // what the outlet actually offers. 1370 var stored = localStorage.getItem('orderType'); 1371 if (enabledIds.length && enabledIds.indexOf(String(stored)) === -1) { 1372 localStorage.setItem('orderType', enabledIds[0]); 1373 updateHeaderInfoBar(); 1374 } 1375 } 1376 1377 function selectOrderTypeAndRedirect(typeId) { 1378 var store = window._lpSelectedStore; 1379 if (!store && state.nearest.length) store = state.nearest[0]; 1380 if (!store) return; 1381 1382 // Delivery is the one type that needs the CUSTOMER's address: it decides 1383 // serviceability and it is where the order goes. The step can now be 1384 // opened on a known outlet alone (changeOrderType above), so this is the 1385 // point where that path collects one instead of committing a delivery 1386 // order with nowhere to deliver it. A pending address counts â it is 1387 // committed by flushPendingAddr() moments later. 1388 // No-op for every pre-existing route here, which only ever arrive with an 1389 // address already in hand. 1390 if (Number(typeId) === 1) { 1391 var addr = localStorage.getItem('userAddress'); 1392 var pending = state.pendingAddr && state.pendingAddr.text; 1393 if (!pending && (!addr || addr === 'null' || addr === '')) { 1394 var udd = userData(); 1395 showLocationStep(udd.token && udd.contactMappingId ? 'savedAddresses' : 'location'); 1396 return; 1397 } 1398 } 1399 // Real outlet + order type confirmed. Wait for any in-flight add-address save so the 1400 // freshly-created deliveryAddressId is committed (not the previously selected one). 1401 // Bound the wait so a slow/failed save can't block the redirect â flushPendingAddr then 1402 // clears the stale id instead of reusing it. 1403 if (state.savePromise) { 1404 var done = false; 1405 var once = function () { if (done) return; done = true; commitAndRedirect(typeId, store); }; 1406 state.savePromise.then(once, once); 1407 setTimeout(once, 2500); 1408 } else { 1409 commitAndRedirect(typeId, store); 1410 } 1411 } 1412 1413 function commitAndRedirect(typeId, store) { 1414 // Commit the held address â this is the only place it reaches the checkout keys. 1415 flushPendingAddr(); 1416 state.savePromise = null; 1417 var slug = store.web_slug || store.slug || ''; 1418 localStorage.setItem('orderType', String(typeId)); 1419 localStorage.setItem('business', String(store.id || '')); 1420 if (slug) localStorage.setItem('slug', slug); 1421 // TTL stamp â 60-min expiry checked at next page load (top of this module). 1422 localStorage.setItem('locationSelectedAt', String(Date.now())); 1423 // CI3 wla_home_20260507.js:2347-2353 â overwrite locality/city with the selected 1424 // outlet's values so the header info bar shows the outlet (not the user's address). 1425 localStorage.setItem('city', store.city || ''); 1426 localStorage.setItem('locality', store.locality || ''); 1427 document.cookie = 'view_business=' + (store.id || '') + '; path=/; max-age=2592000'; 1428 1429 // A serviceable outlet has been committed â every entry path (saved address, new 1430 // address, locality search, GPS) funnels through here, so this is the one place 1431 // that means "user picked where they're ordering from". Bridge, not an import: 1432 // this module's './analytics.js' import is unversioned against a 30-day CDN cache. 1433 try { 1434 window.Analytics?.trackLocationSelected?.({ 1435 businessId: store.id || '', locality: store.locality || '', 1436 city: store.city || '', deliveryType: String(typeId), 1437 }); 1438 } catch (e) { /* never block the redirect */ } 1439 1440 // Resolve-only mode (opt-in via window._lpResolveOnly): the caller wants the 1441 // outlet resolved + committed but WITHOUT navigating away (e.g. the pre-booking 1442 // detail page, which must stay on the event). Commit is already done above;
1443 // close the modal and notify via an event, then bail before the redirect. 1444 // Default (flag unset) behavior is unchanged for every other page. 1445 if (window._lpResolveOnly) { 1446 window._lpResolveOnly = false; 1447 window._lpForceSelect = false; // selection made â allow the modal to close 1448 state.savePromise = null; 1449 closeLocationFlow(); 1450 document.dispatchEvent(new CustomEvent('lp:outlet-resolved', { 1451 detail: { 1452 businessId: String(store.id || ''), 1453 name: store.name || store.businessName || store.brandName || '', 1454 slug: slug, 1455 orderType: typeId, 1456 city: store.city || '', 1457 locality: store.locality || '', 1458 lat: localStorage.getItem('userLat') || '', 1459 lng: localStorage.getItem('userLng') || '', 1460 }, 1461 })); 1462 return; 1463 } 1464 1465 // Populate redirect view 1466 var addrEl = $('#redirectModal_address'); if (addrEl) addrEl.textContent = localStorage.getItem('userAddress') || ''; 1467 var typeEl = $('#redirectModal_orderType'); if (typeEl) typeEl.textContent = orderTypeLabel(typeId); 1468 var outletEl = $('#redirectModal_outlet'); if (outletEl) outletEl.textContent = (store.locality || '') + ', ' + (store.city || ''); 1469 1470 // If a widget-click queued a destination, honor it (CI3 parity). The /online-order 1471 // shortcut becomes the outlet's menu now that we know the slug. 1472 // Reservation (5) â /reservation/{city}/{slug}; Direct Pay (6) â /direct-pay/{slug} 1473 var target = typeId === 5 ? '/reservation/' + encodeURIComponent(store.city || '') + '/' + slug 1474 : typeId === 6 ? '/direct-pay/' + slug 1475 : '/order/' + slug; 1476 if (window._lpPendingNav) { 1477 target = window._lpPendingNav; 1478 window._lpPendingNav = null; 1479 if (/^\/online-order(\/|$|\?)/.test(target)) target = '/order/' + slug; 1480 } 1481 // Silver-widget deep link: append section_slug so the menu jumps to the category. 1482 if (window._lpPendingSection) { 1483 target += (target.indexOf('?') > -1 ? '&' : '?') + 1484 'section_slug=' + encodeURIComponent(window._lpPendingSection); 1485 window._lpPendingSection = null; 1486 } 1487 // Product-page deep link: append item_slug so the menu opens the item popup. 1488 if (window._lpPendingItemSlug) { 1489 target += (target.indexOf('?') > -1 ? '&' : '?') + 1490 'item_slug=' + encodeURIComponent(window._lpPendingItemSlug); 1491 window._lpPendingItemSlug = null; 1492 } 1493 window._lpRedirectTarget = target; 1494 showLocationStep('redirect'); 1495 } 1496 1497 function orderTypeLabel(id) { 1498 // Same canonical set as ORDER_TYPE_LABELS below â this file carried two 1499 // maps that disagreed with each other ("Dine In" here, "Dine In" there, 1500 // "Pickup" vs "Pick Up") as well as with the menu page. 1501 return ({ 1: 'Delivery', 2: 'Pickup', 3: 'Dine-in', 4: 'In-car', 5: 'Reservation', 6: 'Direct Pay' })[id] || ''; 1502 } 1503 1504 // Zomato-style auto-redirect: a progress bar fills over REDIRECT_MS, then navigates. 1505 // The cancel button (cancelRedirect) halts it; the "Change" link still works as before. 1506 var REDIRECT_MS = 3000; 1507 function startRedirectCountdown() { 1508 clearInterval(window._lpCountdownIv); 1509 clearTimeout(window._lpRedirectTimeout); 1510 var loader = $('#redirectLoader'); 1511 if (loader) loader.classList.remove('redirect-loader--cancelled'); 1512 var bar = $('#redirectProgressBar'); 1513 if (bar) { 1514 bar.style.transition = 'none'; 1515 bar.style.width = '0%'; 1516 void bar.offsetWidth; // force reflow so the next change animates 1517 bar.style.transition = 'width ' + REDIRECT_MS + 'ms linear'; 1518 bar.style.width = '100%'; 1519 } 1520 // Live 3 â 2 â 1 countdown in the progress text (matches the filling bar). 1521 var textEl = $('#redirectProgressText'); 1522 var secs = Math.round(REDIRECT_MS / 1000); 1523 var setText = function (n) { if (textEl) textEl.textContent = 'Taking you to the menu in ' + n + '...'; }; 1524 setText(secs); 1525 window._lpCountdownIv = setInterval(function () { 1526 secs -= 1; 1527 if (secs <= 0) { clearInterval(window._lpCountdownIv); return; } 1528 setText(secs); 1529 }, 1000); 1530 window._lpRedirectTimeout = setTimeout(function () { 1531 clearInterval(window._lpCountdownIv); 1532 if (window._lpRedirectTarget) window.location.href = window._lpRedirectTarget; 1533 }, REDIRECT_MS); 1534 } 1535 1536 // Cross beside the progress bar: cancel the auto-redirect and return to the 1537 // order-type step (the previous step) so the user can re-choose. 1538 function cancelRedirect() { 1539 clearTimeout(window._lpRedirectTimeout); 1540 clearInterval(window._lpCountdownIv); 1541 showLocationStep('orderType'); 1542 } 1543 1544 function changeUserLocation() { 1545 clearInterval(window._lpCountdownIv); 1546 clearTimeout(window._lpRedirectTimeout); 1547 state.nearest = []; 1548 // CI3 wla_home_20260507.js:1326 â "DON'T clear localStorage here â data is only 1549 // overwritten when user makes a new selection." Wiping business/slug/deliveryAddressId 1550 // here erased the outlet that drives the header info bar, so opening "Change" and 1551 // closing without picking left the bar with nothing to show (it vanished). Keep the 1552 // prior selection; a new pick overwrites it, a cancel preserves it. 1553 var ud = userData(); 1554 // Logged-in users came from the saved-addresses list; send them back there 1555 // instead of the initial entry step so they can pick a different address. 1556 showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location'); 1557 showOverlay(); 1558 } 1559 1560 // âââ "Explore Menu" (notServiceable step) âââââââââââââââ 1561 // The user's location has no serviceable (in-radius, open) outlet, so getNearestOpenOutlet 1562 // returned nothing and the dead-end "notServiceable" modal is showing. Instead of re-hitting 1563 // that same radius-bound endpoint (which would return null again), fall back to the radius-free 1564 // getNearestOutlet â it returns the closest outlet regardless of radius/open-hours â and send 1565 // the user to its menu so they can still browse. 1566 // Browse-only: we do NOT flush the held address (the location isn't serviceable, so no 1567 // delivery address is committed to checkout) â only the buffer is cleared. 1568 // Last resort: if even getNearestOutlet has nothing (or the call fails), route to the 1569 // store-locator so the button is never a dead end. 1570 function exploreNearestOutlet() { 1571 clearPendingAddr(); 1572 var lat = state.lat || parseFloat(localStorage.getItem('userLat') || '0'); 1573 var lng = state.lng || parseFloat(localStorage.getItem('userLng') || '0'); 1574 1575 var EXPLORE_OUTLET_URL = API_BASE + '/client/getNearestOutlet' 1576 + '?parentBusinessId=' + pId 1577 + '&latitude=' + lat 1578 + '&longitude=' + lng 1579 + (window._lpPrebooking ? '&prebooking=1' : ''); 1580 1581 fetch(EXPLORE_OUTLET_URL, { method: 'GET', headers: apiHeaders(), credentials: 'omit' }) 1582 .then(function (r) { return r.json(); }) 1583 .then(function (d) { 1584 var store = null; 1585 if (d && Number(d.status) === 1 && d.address) { 1586 store = Array.isArray(d.address) ? d.address[0] : d.address; 1587 } else if (d && (d.data || d.rows)) { 1588 var arr = d.data || d.rows; 1589 store = Array.isArray(arr) ? arr[0] : arr; 1590 } 1591 var slug = store && (store.web_slug || store.slug || ''); 1592 if (slug) { 1593 // Outlet IS selected (the user is browsing this menu), even though no 1594 // serviceable DELIVERY location was set. Persist the outlet tuple so 1595 // home/product don't re-prompt â mirrors the store-locator .sl-cta--primary 1596 // handler below (outlet picked â user location set): store business/slug/ 1597 // city/locality, clear the stale (non-serviceable) user-location keys so 1598 // "Your Location" reads "No location selected" and changeOrderType() routes 1599 // to the address step. No address is flushed and no order type is set, so 1600 // checkout still asks for a real deliverable address. 1601 localStorage.setItem('slug', slug); 1602 localStorage.setItem('business', String(store.id || '')); 1603 if (store.city) localStorage.setItem('city', store.city); 1604 if (store.locality) localStorage.setItem('locality', store.locality); 1605 localStorage.removeItem('userLat'); 1606 localStorage.removeItem('userLng'); 1607 localStorage.removeItem('userLocSource'); 1608 localStorage.removeItem('userAddress'); 1609 localStorage.removeItem('userAddressLabel'); 1610 localStorage.removeItem('deliveryAddressId'); 1611 localStorage.removeItem('selectedDeliveryAddressId'); 1612 localStorage.setItem('locationSelectedAt', String(Date.now())); 1613 if (store.id) document.cookie = 'view_business=' + store.id + '; path=/; max-age=2592000'; 1614 } 1615 window.location.href = slug ? '/order/' + slug : '/store-locator'; 1616 }) 1617 .catch(function () { window.location.href = '/store-locator'; }); 1618 } 1619 1620 // âââ Map adapters (Google / Leaflet) ââââââââââââââââââââ 1621 function loadLeaflet() { 1622 if (window.L && window.L.map) return Promise.resolve(); 1623 return new Promise(function (resolve) { 1624 var link = document.createElement('link'); 1625 link.rel = 'stylesheet'; link.href = 'https://unpkg.com/[email protected]/dist/leaflet.css'; 1626 document.head.appendChild(link); 1627 var s = document.createElement('script'); 1628 s.src = 'https://unpkg.com/[email protected]/dist/leaflet.js'; 1629 s.onload = resolve; document.head.appendChild(s); 1630 }); 1631 } 1632 1633 function loadGoogleMaps() { 1634 if (window.google && window.google.maps && window.google.maps.Map) return Promise.resolve(); 1635 if (window._gmapsPromise) return window._gmapsPromise; 1636 window._gmapsPromise = new Promise(function (resolve) { 1637 window._gmapsReady = resolve; 1638 var s = document.createElement('script');
1639 s.src = 'https://maps.googleapis.com/maps/api/js?key=' + encodeURIComponent(mapsKey) + '&libraries=places&loading=async&callback=_gmapsReady'; 1640 document.head.appendChild(s); 1641 }); 1642 return window._gmapsPromise; 1643 } 1644 1645 var mapInstance = { kind: null, map: null, marker: null }; 1646 1647 // Teardrop pin shared by both map engines (no dependency on Leaflet's default icon 1648 // images). Fixed green for now per design; swap to var(--brand) to theme per brand. 1649 function brandPinSvg() { 1650 var c = '#22c55e'; 1651 return '<svg xmlns="http://www.w3.org/2000/svg" width="34" height="44" viewBox="0 0 34 44">' 1652 + '<path d="M17 0C7.6 0 0 7.6 0 17c0 11.9 15.3 25.6 16 26.2.6.5 1.4.5 2 0C18.7 42.6 34 28.9 34 17 34 7.6 26.4 0 17 0z" fill="' + c + '"/>' 1653 + '<circle cx="17" cy="17" r="6" fill="#fff"/></svg>'; 1654 } 1655 1656 function initAddressMap() { 1657 // CI3 wla_home_20260507.js:1577-1583 â seed the map center from stored coords so the 1658 // form opens on the user's last-known location, not the Delhi fallback. 1659 var sLat = parseFloat(localStorage.getItem('userLat') || ''); 1660 var sLng = parseFloat(localStorage.getItem('userLng') || ''); 1661 var hasStored = isFinite(sLat) && isFinite(sLng) && !(sLat === 0 && sLng === 0); 1662 var lat = state.lat || (hasStored ? sLat : 28.6139); 1663 var lng = state.lng || (hasStored ? sLng : 77.2090); 1664 // CI3 wla_home_20260507.js:1634 â hydrate the form from the initial coords on open so 1665 // locality/city/address show immediately. Add flow only (edit pre-fills its own saved 1666 // address); when no coords exist the GPS auto-fetch in openHomeAddressForm handles it. 1667 var hydrateOnInit = !manageMode && (!!state.lat || hasStored); 1668 if (useGoogle) { 1669 var el = document.getElementById('homeAddressMap'); 1670 if (!el) return; 1671 if (mapInstance.map && mapInstance.kind === 'google') { 1672 applyMapCenter(lat, lng); 1673 if (hydrateOnInit) hydrateFormFromLatLng(lat, lng); 1674 return; 1675 } 1676 loadGoogleMaps().then(function () { 1677 var map = new google.maps.Map(el, { 1678 center: { lat: lat, lng: lng }, zoom: 16, 1679 mapTypeControl: false, streetViewControl: false, fullscreenControl: false, 1680 // 'greedy' so one-finger drag pans the map on mobile (default 'auto' is 1681 // cooperative â one finger scrolls the page, blocking the drag-the-map pin UX). 1682 gestureHandling: 'greedy', clickableIcons: false 1683 }); 1684 mapInstance.kind = 'google'; mapInstance.map = map; 1685 // Centre-pin UX: the pin is a static CSS element (.addr-map__pin) fixed at the map 1686 // centre, so the map moves under it (drag-the-map). Replaces the deprecated 1687 // google.maps.Marker. `idle` fires after the map settles; dedupe skips programmatic/ 1688 // tile moves; onPinChanged debounces (400ms) so panning fires ONE geocode. 1689 map.addListener('idle', function () { 1690 var ctr = map.getCenter(); if (!ctr) return; 1691 var cl = ctr.lat(), cn = ctr.lng(); 1692 if (mapInstance.suppressGeo) return; // programmatic resize â don't re-geocode 1693 if (state.lat != null && Math.abs(cl - state.lat) < 1e-5 && Math.abs(cn - state.lng) < 1e-5) return; 1694 onPinChanged(cl, cn); 1695 }); 1696 map.addListener('click', function (e) { map.panTo(e.latLng); }); 1697 wireGooglePlaces(); 1698 if (hydrateOnInit) hydrateFormFromLatLng(lat, lng); 1699 }); 1700 } else { 1701 var el2 = document.getElementById('homeLeafletMap'); 1702 if (!el2) return; 1703 if (mapInstance.map && mapInstance.kind === 'leaflet') { 1704 applyMapCenter(lat, lng); 1705 if (hydrateOnInit) hydrateFormFromLatLng(lat, lng); 1706 return; 1707 } 1708 loadLeaflet().then(function () { 1709 el2.innerHTML = ''; 1710 var map = L.map(el2, { zoomControl: true }
1710).setView([lat, lng], 16); 1711 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap' }).addTo(map); 1712 mapInstance.kind = 'leaflet'; mapInstance.map = map; 1713 // Draggable marker (custom divIcon so we don't depend on Leaflet's default 1714 // marker images). Drag to drop, or tap the map to move the pin there. 1715 var pinIcon = L.divIcon({ className: 'lp-leaflet-pin', html: brandPinSvg(), iconSize: [34, 44], iconAnchor: [17, 44] }); 1716 var marker = L.marker([lat, lng], { draggable: true, icon: pinIcon }).addTo(map); 1717 mapInstance.marker = marker; 1718 // Centre-pin UX (see Google branch above): any map movement snaps the pin to the 1719 // new centre and geocodes it, keeping pin + fixed coach-mark aligned. onPinChanged 1720 // debounces (400ms) so panning fires ONE geocode; dedupe skips no-op/resize moves. 1721 map.on('moveend', function () { 1722 var c = map.getCenter(); 1723 marker.setLatLng(c); // always keep pin centred 1724 if (mapInstance.suppressGeo) return; // programmatic resize â don't re-geocode 1725 if (state.lat != null && Math.abs(c.lat - state.lat) < 1e-5 && Math.abs(c.lng - state.lng) < 1e-5) return; 1726 onPinChanged(c.lat, c.lng); 1727 }); 1728 map.on('click', function (e) { map.panTo(e.latlng); }); 1729 marker.on('dragend', function () { map.panTo(marker.getLatLng()); }); 1730 setTimeout(resizeHomeMap, 200); 1731 if (hydrateOnInit) hydrateFormFromLatLng(lat, lng); 1732 }); 1733 } 1734 } 1735 1736 function applyMapCenter(lat, lng) { 1737 if (!mapInstance.map) return; 1738 if (mapInstance.kind === 'google') { 1739 mapInstance.map.setCenter({ lat: lat, lng: lng }); 1740 if (mapInstance.marker) mapInstance.marker.setPosition({ lat: lat, lng: lng }); 1741 } else { 1742 mapInstance.map.setView([lat, lng], 16); 1743 if (mapInstance.marker) mapInstance.marker.setLatLng([lat, lng]); 1744 setTimeout(resizeHomeMap, 100); 1745 } 1746 } 1747 1748 function wireGooglePlaces() { 1749 var inp = document.getElementById('homeAddressSearch'); 1750 if (!inp || !window.google || !google.maps || !google.maps.places) return; 1751 var ac = new google.maps.places.Autocomplete(inp, { fields: ['geometry', 'address_components', 'formatted_address'], componentRestrictions: { country: 'IN' } }); 1752 ac.addListener('place_changed', function () { 1753 var p = ac.getPlace(); 1754 if (!p || !p.geometry) return; 1755 var lat = p.geometry.location.lat(); 1756 var lng = p.geometry.location.lng(); 1757 persistCoords(lat, lng, 'address'); 1758 applyMapCenter(lat, lng); 1759 state.address = p.formatted_address || state.address; 1760 if (state.address) localStorage.setItem('userAddress', state.address); 1761 // CI3 wla_home_20260507.js:1662-1671 â a search pick is equivalent to a pin 1762 // drop. The Place already carries address_components, so hydrate locality/city/ 1763 // state/pincode straight from it (no extra geocode) and unlock Save. No add-time 1764 // serviceability gate â that's decided at checkout. Without this the picked place 1765 // recentred the map but Locality/City stayed empty and Save stayed disabled. 1766 state.lat = lat; state.lng = lng; 1767 applyAddressComponents(p.address_components); 1768 applyGeocodeToForm(lat, lng); 1769 // The picked place STAYS in the field. Clearing it left nothing on screen 1770 // saying what was searched for, so a wrong result could not be recognised 1771 // as wrong, let alone edited â the info box shows where the pin landed, not 1772 // what was asked for. Keeping the query is what makes the two comparable. 1773 }); 1774 } 1775 1776 var pinDebounce = null; 1777 function onPinChanged(lat, lng) { 1778 clearTimeout(pinDebounce);
1779 // In-memory only â do NOT call persistCoords here. localStorage writes happen on 1780 // explicit user confirmation (GPS pick, locality pick, saved address, Save Address). 1781 // CI3 wla_home_20260507.js:1604-1612 â drag/click funnel into the same form-hydrate 1782 // path as search; Google brands add unconditionally, serviceability waits for checkout. 1783 pinDebounce = setTimeout(function () { hydrateFormFromLatLng(lat, lng); }, 400); 1784 } 1785 1786 // âââ Header location info bar (CI3-aligned) âââââââââââââ 1787 // CANONICAL order-type labels â see the note in headerV2.js. This bar is the 1788 // mobile counterpart of the header's order-type chip, so a different 1789 // spelling here just moves the mismatch to a breakpoint. Kept as a literal 1790 // copy rather than a shared import on purpose (cached /src/js, unversioned 1791 // nested imports). 1792 var ORDER_TYPE_LABELS = { 1: 'Delivery', 2: 'Pickup', 3: 'Dine-in', 4: 'In-car', 5: 'Reservation', 6: 'Direct Pay' }; 1793 // The types where the customer goes to the OUTLET rather than the order 1794 // coming to them â so the bar names the outlet, not their saved address. 1795 // Literal copy of headerV2.js's map for the same reason as the labels above. 1796 var AT_OUTLET_TYPES = { 2: 1, 3: 1, 4: 1, 5: 1, 6: 1 }; 1797 1798 function updateHeaderInfoBar() { 1799 var bar = document.getElementById('locationInfoBar'); 1800 if (!bar) return; 1801 1802 var addr = localStorage.getItem('userAddress') || ''; 1803 var addrLabel = localStorage.getItem('userAddressLabel') || ''; 1804 // Defensive: the checkout hydration path (and older builds) could store a raw 1805 // numeric address-type id here ("1" = Home, "2" = Office, â¦). The bar renders 1806 // this verbatim, so map a bare number to its label â otherwise it shows "1". 1807 if (/^\d+$/.test(addrLabel)) { 1808 var lblNum = parseInt(addrLabel, 10); 1809 addrLabel = (lblNum !== 0 && TYPE_LABELS[lblNum]) ? TYPE_LABELS[lblNum] : ''; 1810 } 1811 var locality = localStorage.getItem('locality'); 1812 var city = localStorage.getItem('city'); 1813 var business = localStorage.getItem('business'); 1814 var slug = localStorage.getItem('slug'); 1815 var orderType = localStorage.getItem('orderType'); 1816 1817 var has = function (v) { return v && v !== 'null' && v !== 'undefined'; }; 1818 var hasLocation = (has(locality) && has(city) && (has(business) || has(slug))) || has(slug); 1819 1820 // v2's bar is the ONLY location control below 992px â the header chip is 1821 // d-none d-lg-flex and a phone row has no space for it. Hiding the bar 1822 // outright when nothing is stored therefore left a FIRST-TIME mobile 1823 // visitor with no way to set a location at all, while desktop still got 1824 // the chip's "Set location". So v2's bar (marked d-lg-none by base.php) 1825 // stays up in a prompt state instead. v1 keeps hiding: its own entry 1826 // points cover that case and its bar is not breakpoint-scoped. 1827 var isV2Bar = bar.classList.contains('d-lg-none'); 1828 var outletSec = bar.querySelector('.location-info-bar-divider'); 1829 var typeSec = bar.querySelector('.location-info-bar-section-fixed'); 1830 1831 if (!hasLocation) { 1832 if (!isV2Bar) { bar.style.display = 'none'; return; } 1833 var promptEl = bar.querySelector('#headerAddress'); 1834 if (promptEl) promptEl.textContent = 'Select location'; 1835 var promptLbl = bar.querySelector('#headerAddressLabel'); 1836 if (promptLbl) promptLbl.style.display = 'none'; 1837 // The ORDER TYPE is still reported here. It is stored independently of 1838 // the location â the menu page writes the outlet's default on load 1839 // (menu.js), and the journey auto-commits a sole enabled type â so 1840 // falling straight through left every other page showing "Set order 1841 // type" over a choice that had in fact been made, while the menu page 1842 // a click earlier showed "Pickup". 1843 Array.prototype.forEach.call(bar.querySelectorAll('.headerOrderType'), function (el) { 1844 el.textContent = ORDER_TYPE_LABELS[orderType] || 'Set order type'; 1845 }); 1846 // The outlet row says nothing before a location exists, and an empty 1847 // "Selected Outlet" row reads as a rendering fault. v1's fixed 1848 // order-type section goes with it: v1 is not being changed here, and 1849 // its bar has its own rows for this. 1850 if (outletSec) outletSec.style.display = 'none'; 1851 if (typeSec) typeSec.style.display = 'none'; 1852 bar.style.display = ''; 1853 return; 1854 } 1855 // Restore whatever the prompt state hid above. 1856 if (outletSec) outletSec.style.display = ''; 1857 if (typeSec) typeSec.style.display = ''; 1858 1859 var displayLocality = has(locality) ? locality : (slug || ''); 1860 var displayCity = has(city) ? city : ''; 1861 var displayOutlet = displayLocality + (displayCity ? ', ' + displayCity : ''); 1862 1863 var addrEl = bar.querySelector('#headerAddress'); 1864 var lblEl = bar.querySelector('#headerAddressLabel'); 1865 var outEl = bar.querySelector('#headerOutlet'); 1866 var typeEls = bar.querySelectorAll('.headerOrderType'); 1867 1868 // v1 has a separate "Selected Outlet" row to fall back on, so an empty 1869 // address there can read "No location selected". v2's bar is one line 1870 // with no such row, so it falls back the way the desktop chip does â 1871 // address, then locality/city, then the outlet slug â and only says
1872 // "Select location" when there is genuinely nothing. 1873 // v2 ONLY. v1 is deliberately left alone: it has a separate "Selected Outlet" 1874 // row, so its "Your Location" honestly reads "No location selected" here and 1875 // never mislabelled the outlet as the user's address â verified against the 1876 // same state. v2 collapsed both into one chip, which is what made the outlet 1877 // fallback wrong. Nothing to fix on v1, and ~2,500 brands run it. 1878 if (addrEl) { 1879 // WHICH place this names depends on the order type, exactly as the 1880 // header chip's does (headerV2.js refreshLocation): delivery comes to 1881 // the customer, so it names their address; pickup, dine-in, in-car and 1882 // reservation happen AT the outlet, so naming their home address there 1883 // is simply false. The bar showed the address for every type, so a 1884 // visitor who chose pickup on the home page read "Set location" â or 1885 // worse, their own street â under a heading that said "Pick up from", 1886 // while the menu page's band (the same chip, reflowed) named the 1887 // outlet. Same words, same source, both widths. 1888 addrEl.textContent = isV2Bar 1889 ? ((AT_OUTLET_TYPES[orderType] ? displayOutlet : addr) || 'Select location') 1890 : (addr || 'No location selected'); 1891 } 1892 if (lblEl) { 1893 if (addr && addrLabel) { lblEl.textContent = addrLabel; lblEl.style.display = ''; } 1894 else { lblEl.style.display = 'none'; } 1895 } 1896 if (outEl) outEl.textContent = displayOutlet; 1897 // Same prompt as the desktop chip when no type is stored. v2 ONLY: v1's bar 1898 // keeps its 'Delivery' default, since v1 is not being changed here. 1899 Array.prototype.forEach.call(typeEls, function (el) { 1900 el.textContent = ORDER_TYPE_LABELS[orderType] || (isV2Bar ? 'Set order type' : 'Delivery'); 1901 }); 1902 bar.style.display = ''; 1903 } 1904 1905 function changeOrderType() { 1906 // CI3 wla_home_20260507.js:1402-1429. The order-type step is only correct once the user 1907 // has entered their OWN location (userAddress). An outlet picked from the store locator 1908 // sets business/slug but no userAddress, so there's nothing to anchor the nearest-outlet 1909 // lookup â collect the location first: logged-in users see their saved-address listing, 1910 // anonymous users the entry step (CI3 changeOrderType :1410-1413 â changeUserLocation 1911 // :1327-1331). Don't wipe the stored outlet (CI3 :1403-1404) â if the user opens "Change" 1912 // and closes without picking anything, their current selection survives. 1913 var ud = userData(); 1914 var business = localStorage.getItem('business'); 1915 var hasOutlet = business && business !== 'null'; 1916 var addr = localStorage.getItem('userAddress'); 1917 var hasAddress = addr && addr !== 'null' && addr !== ''; 1918 1919 // ...or when the outlet itself is known, even without an address: its own 1920 // flags say which types it offers, and for pickup/dine-in/in-car there is 1921 // no address to anchor anything to. Delivery still collects one â see the 1922 // guard in selectOrderTypeAndRedirect(). 1923 if (hasOutlet && (hasAddress || storedOutletCard())) { 1924 showLocationStep('orderType'); 1925 } else { 1926 showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location'); 1927 } 1928 showOverlay(); 1929 } 1930 1931 // âââ Public globals (referenced from modal markup) ââââââ 1932 window.closeLocationFlow = closeLocationFlow; 1933 window.CurrentAddress = CurrentAddress; 1934 window.showLocationStep = showLocationStep; 1935 // showOverlay is needed alongside showLocationStep when external callers 1936 // (e.g. productPage.js "+ Add") want to actually display the modal â without 1937 // it the step is set but the modal stays hidden. 1938 window.showLocationOverlay = showOverlay; 1939 window.enterAddressSearchLocality = enterAddressSearchLocality; 1940 window.homeSearchLocality = homeSearchLocality; 1941 window.openHomeAddressForm = openHomeAddressForm; 1942 window.openEditAddressForm = openEditAddressForm; 1943 window.homeAddrGoToCurrentLocation = homeAddrGoToCurrentLocation; 1944 window.confirmLocationAndShowForm = confirmLocationAndShowForm; 1945 window.homeChangeOnMap = homeChangeOnMap; 1946 window.backToLocationMap = backToLocationMap; 1947 window.closeAddressForm = closeAddressForm; 1948 window.saveHomeAddress = saveHomeAddress; 1949 window.selectOrderTypeAndRedirect = selectOrderTypeAndRedirect; 1950 window.openLoginOrSavedAddresses = openLoginOrSavedAddresses; 1951 window.changeUserLocation = changeUserLocation; 1952 window.exploreNearestOutlet = exploreNearestOutlet; 1953 window.cancelRedirect = cancelRedirect; 1954 window.changeOrderType = changeOrderType; 1955 1956 // âââ Widget click gate ââââââââââââââââââââââââââââââââââ 1957 // Intercept clicks on anchors targeting outlet-dependent paths. If the user has no outlet 1958 // (slug + business), open the modal and queue the destination for after order-type pick. 1959 // If the user has an outlet, /online-order shortcuts straight to /order/{slug} (CI3 parity, 1960 // wla_home_20260507.js:638-700). Other gated hrefs proceed normally. 1961 // Product pages (/products, /product-detail/...) are intentionally NOT gated â 1962 // users should browse the catalogue freely; the location modal is triggered by 1963 // productPage.js when they click "+ Add" on an item (the action that actually 1964 // needs an outlet). 1965 // `/online-order` is intentionally NOT gated â it's the store-locator page where 1966 // anonymous users browse outlets. Picking an outlet there sets localStorage via the 1967 // .sl-cta--primary handler below. 1968 // Single-outlet brands: seed the one outlet so the header has something to 1969 // show. Nothing else does it any more â the disabled-flow branch that used to 1970 // is no longer taken now that these brands run the journey, and arriving on 1971 // /order/{slug} directly writes no location at all. The visible symptom was a 1972 // visitor going straight to the menu, coming back to home, and finding the 1973 // chip on "Set location" for a brand with exactly one outlet. 1974 // 1975 // The two keys are seeded on different rules, because they answer to different 1976 // owners: 1977 // 1978 // slug â always overwritten, as the config-0 branch above does and for the 1979 // same reason: with one outlet the only correct slug is this one, so a 1980 // value left over from a RENAMED outlet is a stale cache, not a choice. 1981 // Leaving it strands the visitor on /order/{unknown-slug}, which 302s back 1982 // to home and reads as "the category click just reloads the page". Safe to 1983 // stamp even after a journey pick â that journey can only have resolved to 1984 // this same single outlet. 1985 // 1986 // city/locality â only when unset. These are the VISITOR's, written by the 1987 // journey from the address they chose. Re-stamping the outlet's over the 1988 // top on the next page load would quietly replace where they are 1989 // delivering to with where the shop is. 1990 (function _seedSoleOutlet() { 1991 if (Number(AppConfig.storesCount) !== 1) return; 1992 1993 var d = window.storesDetails; 1994 if (!d || !d.length) return; 1995 var store = d[0]; 1996 var slug = store.web_slug || store.slug || ''; 1997 if (!slug) return; 1998 1999 localStorage.setItem('slug', slug); 2000 2001 var haveCity = localStorage.getItem('city'); 2002 var haveLocality = localStorage.getItem('locality'); 2003 if (store.city && (!haveCity || haveCity === 'null')) localStorage.setItem('city', store.city); 2004 if (store.locality && (!haveLocality || haveLocality === 'null')) localStorage.setItem('locality', store.locality); 2005 2006 if (typeof window.hdr2RefreshHeader === 'function') window.hdr2RefreshHeader(); 2007 }()); 2008 2009 // âââ Adopt the outlet whose menu we are on ââââââââââââââ 2010 // A visitor who lands on /order/{slug} from a shared link, a search result or 2011 // a QR code never runs the location journey, so nothing is stored. The MENU 2012 // page still showed the outlet, because the header chip falls back to 2013 // window.OutletConfig (headerV2.js) â but every other page has no such 2014 // fallback, so the same header went from "Pick up from · DLF Midtown Plaza" 2015 // to "Set location" on the next click, as if the visit had been forgotten. 2016 // 2017 // The page is the server's own statement of which outlet this is, so record 2018 // it, exactly as checkout.js does from CheckoutConfig and as the single-outlet
2019 // seeder above does from storesDetails. Same two rules as that seeder: 2020 // 2021 // All four keys are stamped, not merged. commitAndRedirect() above already 2022 // overwrites city/locality with the committed OUTLET's values on every 2023 // journey (CI3 wla_home_20260507.js:2347-2353) precisely so the header names 2024 // the outlet â the visitor's own address lives in userAddress, which nothing 2025 // here touches. Merging instead would leave a visitor who committed outlet A 2026 // and then opened outlet B's menu reading "Pick up from <A's locality>" over 2027 // B's menu. Being on an outlet's page is the same assertion as picking it. 2028 (function _seedCurrentOutlet() { 2029 // v2 headers only. The seeding would be just as correct under v1, but v1 2030 // runs on every live brand and slug/business are state the cart and 2031 // checkout read â that is not a change to make on the whole fleet to fix 2032 // a mismatch reported in the v2 header. Drop this line to widen it. 2033 if (!document.querySelector('.site-header.hdr2')) return; 2034 2035 var oc = window.OutletConfig; 2036 if (!oc || !oc.slug) return; 2037 2038 localStorage.setItem('slug', String(oc.slug)); 2039 if (oc.businessId) localStorage.setItem('business', String(oc.businessId)); 2040 2041 if (oc.city) localStorage.setItem('city', String(oc.city)); 2042 if (oc.locality) localStorage.setItem('locality', String(oc.locality)); 2043 2044 // Also keep the outlet as a STORE RECORD, in the shape renderOrderTypes() 2045 // and commitAndRedirect() already take. Without it the order-type step is 2046 // unreachable off the menu page: it is built around "find the outlet 2047 // nearest these coordinates", and a visitor who arrived on /order/{slug} 2048 // from a link has no coordinates, so loadOrderTypes() bounced them to 2049 // address entry â while the menu page a click earlier switched order type 2050 // instantly. Same outlet, same choice, two different journeys. 2051 // 2052 // OutletConfig.orderTypes already carries the four flags, so this costs no 2053 // request and no view change. Keyed by business id and checked on read, so 2054 // a later journey pick (which writes a different `business`) retires it 2055 // rather than answering for the wrong outlet. 2056 var ot = oc.orderTypes || {}; 2057 try { 2058 localStorage.setItem(OUTLET_CARD_KEY, JSON.stringify({ 2059 id: String(oc.businessId || ''), 2060 slug: String(oc.slug || ''), 2061 city: String(oc.city || ''), 2062 locality: String(oc.locality || ''), 2063 onlineOrdersDelivery: Number(ot.delivery) === 1 ? 1 : 0, 2064 onlineDeliveryOn: Number(ot.delivery) === 1 ? 1 : 0, 2065 onlineOrdersSelfPickup: Number(ot.pickup) === 1 ? 1 : 0, 2066 dineInOrders: Number(ot.dine_in) === 1 ? 1 : 0, 2067 inCarOrders: Number(ot.in_car) === 1 ? 1 : 0, 2068 })); 2069 } catch (e) { /* storage full or blocked â the step just falls back */ } 2070 2071 updateHeaderInfoBar(); 2072 if (typeof window.hdr2RefreshHeader === 'function') window.hdr2RefreshHeader(); 2073 }()); 2074 2075 var GATED_HREF_RE = /^\/(order\/|cart|checkout)(\/|$|\?)/; 2076 2077 document.addEventListener('click', function (e) { 2078 if (e.defaultPrevented) return; 2079 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 2080 var a = e.target.closest('a[href]'); 2081 if (!a) return; 2082 if (a.target === '_blank') return; 2083 // Store-locator cards are the explicit outlet-pick surface â never gate them. 2084 if (a.closest('.sl-card')) return; 2085 var href = a.getAttribute('href') || ''; 2086 if (!GATED_HREF_RE.test(href)) return; 2087 // One outlet â nothing for this gate to resolve. A /order/ link already 2088 // names the only outlet there is, so stopping it to ask "where?" gates a 2089 // choice the brand has already made. Those brands keep the journey on the 2090 // header controls; every other route in runs unimpeded. /cart and 2091 // /checkout follow the same reasoning â they act on a cart that can only 2092 // belong to that one outlet. 2093 //
2094 // This also preserves how those brands behaved before they were given the 2095 // journey: their config was forced to 0, so this module returned early and 2096 // the gate was never installed for them at all. 2097 if (Number(AppConfig.storesCount) === 1) return; 2098 2099 var slug = localStorage.getItem('slug'); 2100 var business = localStorage.getItem('business'); 2101 var hasOutletPick = slug && slug !== 'null' && business && business !== 'null'; 2102 2103 if (hasOutletPick) return; // gated paths proceed naturally when outlet is set 2104 2105 e.preventDefault(); 2106 window._lpPendingNav = href; 2107 var ud = userData(); 2108 showLocationStep(ud.token && ud.contactMappingId ? 'savedAddresses' : 'location'); 2109 showOverlay(); 2110 }, true); 2111 2112 // Store-locator outlet-pick â when user clicks the primary "Order Online" CTA 2113 // OR taps anywhere on the card (whole-card click in storeLocator.js fires a 2114 // synthetic click on the primary link, which bubbles here), persist the outlet 2115 // to localStorage so the home info bar shows the selection. Let the link 2116 // navigate normally afterwards (to /order/{slug}). 2117 document.addEventListener('click', function (e) { 2118 var btn = e.target.closest('.sl-cta--primary'); 2119 if (!btn) return; 2120 var card = btn.closest('.sl-card'); 2121 if (!card) return; 2122 var d = card.dataset || {}; 2123 var cardSlug = d.slug || ''; 2124 var cardBusiness = d.business || ''; 2125 if (!cardSlug || !cardBusiness) return; 2126 // Display-friendly locality (with original casing) when available, lowercase fallback. 2127 var locality = d.localityDisplay || d.locality || ''; 2128 var city = d.city || ''; 2129 localStorage.setItem('slug', cardSlug); 2130 localStorage.setItem('business', cardBusiness); 2131 if (locality) localStorage.setItem('locality', locality); 2132 if (city) localStorage.setItem('city', city); 2133 // Picking an outlet is NOT the user entering their own delivery location (CI3 2134 // wla_home_20260507.js:3111-3114 stores only business/slug/city/locality â no coords, 2135 // no userAddress). So clear any stale user address (and the saved-address id it came 2136 // from): "Your Location" must read "No location selected" (CI3 header.php:564), and 2137 // changeOrderType() must route to the address step â not the order-type step â until 2138 // the user actually sets their location. 2139 localStorage.removeItem('userLat'); 2140 localStorage.removeItem('userLng'); 2141 localStorage.removeItem('userLocSource'); 2142 localStorage.removeItem('userAddress'); 2143 localStorage.removeItem('userAddressLabel'); 2144 localStorage.removeItem('deliveryAddressId'); 2145 localStorage.removeItem('selectedDeliveryAddressId'); 2146 localStorage.setItem('locationSelectedAt', String(Date.now())); 2147 document.cookie = 'view_business=' + cardBusiness + '; path=/; max-age=2592000'; 2148 updateHeaderInfoBar(); 2149 }, true); 2150 2151 // Silver-theme widget handlers â `silverCatClick(section, outletSlug, noOfStores)` is 2152 // inlined by widget partials and already includes its own location gate, but it doesn't 2153 // queue a post-pick destination; `silverItemClick(section, itemSlug, bId)` is referenced 2154 // in markup but NOT defined inline (broken in the legacy template), so the +Add buttons 2155 // on home item cards silently fail. We override both with versions that: 2156 // ⢠If location set â navigate to /order/{userOutlet}?section_slug&item_slug 2157 // ⢠If no location â queue section + item, open the modal; redirect happens after 2158 // order-type selection (selectOrderTypeAndRedirect consumes _lpPendingSection/Item). 2159 // One outlet â a tile has nothing to disambiguate, so it goes straight to that 2160 // outlet's menu instead of opening the journey. Those brands DO run the journey 2161 // now (base.php stopped forcing their config to 0), but it belongs to the header 2162 // controls alone: "where do I deliver to" and "which order type" are worth asking 2163 // on demand, while a category tile asking them first is a gate in front of a 2164 // choice that was already made by the brand having a single store. 2165 // 2166 // Slug sources in order: the one the widget passed, the home page's 2167 // storesDetails, then localStorage. Returns '' when none resolves, and the 2168 // callers fall through to the normal gate rather than navigating to /order/. 2169 function _soleOutletSlug(passed) { 2170 if (Number(AppConfig.storesCount) !== 1) return ''; 2171 if (passed && passed !== 'null') return String(passed); 2172 var d = window.storesDetails; 2173 if (d && d.length) { 2174 var slug = d[0].web_slug || d[0].slug || ''; 2175 if (slug) return String(slug); 2176 } 2177 var stored = localStorage.getItem('slug'); 2178 return (stored && stored !== 'null') ? String(stored) : ''; 2179 } 2180 2181 function _gatedSilverCatClick(sectionSlug, outletSlug, noOfStores) { 2182 var sole = _soleOutletSlug(outletSlug); 2183 if (sole) { 2184 var soleUrl = '/order/' + sole; 2185 if (sectionSlug) soleUrl += '?section_slug=' + encodeURIComponent(sectionSlug); 2186 window.location.href = soleUrl; 2187 return; 2188 } 2189 var s = localStorage.getItem('slug'); 2190 var b = localStorage.getItem('business'); 2191 var ok = s && s !== 'null' && b && b !== 'null';
2192 if (!ok) { 2193 if (sectionSlug) window._lpPendingSection = sectionSlug; 2194 var u = userData(); 2195 showLocationStep(u.token && u.contactMappingId ? 'savedAddresses' : 'location'); 2196 showOverlay(); 2197 return; 2198 } 2199 var target = (Number(noOfStores) === 1 && outletSlug && outletSlug !== 'null') ? outletSlug : s; 2200 var url = '/order/' + target; 2201 if (sectionSlug) url += '?section_slug=' + encodeURIComponent(sectionSlug); 2202 window.location.href = url; 2203 } 2204 2205 function _gatedSilverItemClick(sectionSlug, itemSlug, businessId) { 2206 // Third arg is a businessId, not a slug, so nothing to pass through here â 2207 // _soleOutletSlug falls back to storesDetails (see home.php). 2208 var sole = _soleOutletSlug(''); 2209 if (sole) { 2210 var soleQs = []; 2211 if (sectionSlug) soleQs.push('section_slug=' + encodeURIComponent(sectionSlug)); 2212 if (itemSlug) soleQs.push('item_slug=' + encodeURIComponent(itemSlug)); 2213 window.location.href = '/order/' + sole + (soleQs.length ? '?' + soleQs.join('&') : ''); 2214 return; 2215 } 2216 var s = localStorage.getItem('slug'); 2217 var b = localStorage.getItem('business'); 2218 var ok = s && s !== 'null' && b && b !== 'null'; 2219 if (!ok) { 2220 if (sectionSlug) window._lpPendingSection = sectionSlug; 2221 if (itemSlug) window._lpPendingItemSlug = itemSlug; 2222 var u = userData(); 2223 showLocationStep(u.token && u.contactMappingId ? 'savedAddresses' : 'location'); 2224 showOverlay(); 2225 return; 2226 } 2227 var url = '/order/' + s; 2228 var qs = []; 2229 if (sectionSlug) qs.push('section_slug=' + encodeURIComponent(sectionSlug)); 2230 if (itemSlug) qs.push('item_slug=' + encodeURIComponent(itemSlug)); 2231 if (qs.length) url += '?' + qs.join('&'); 2232 window.location.href = url; 2233 } 2234 2235 function _installSilverWidgetHandlers() { 2236 window.silverCatClick = _gatedSilverCatClick; 2237 window.silverItemClick = _gatedSilverItemClick; 2238 } 2239 // Replace BEFORE DOMContentLoaded so inline definitions later in the page can't override 2240 // ours â and again after, in case they do (script load order varies). Both stages are 2241 // idempotent because we install the same reference. 2242 _installSilverWidgetHandlers(); 2243 if (document.readyState === 'loading') { 2244 document.addEventListener('DOMContentLoaded', _installSilverWidgetHandlers); 2245 } 2246 window.addEventListener('load', _installSilverWidgetHandlers); 2247 2248 // Home widget menu-item / ADD-button gate â these cards render as <article> without 2249 // an href, so the URL gate above misses them. On the home page (only) intercept clicks 2250 // on .add-ctrl__btn / .wla-best__btn and .item / .wla-best__card card bodies so they 2251 // participate in the location flow (open modal if no outlet, else jump to /order/{slug} 2252 // with the item preselected â never /product-detail from the home rails). 2253 // On /products and /product-detail/* this is handled by productPage.js â skip there. 2254 document.addEventListener('click', function (e) { 2255 if (e.defaultPrevented) return; 2256 if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 2257 // Active only on the home route; other pages have their own +Add wiring. 2258 var path = window.location.pathname || '/'; 2259 if (path !== '/' && path !== '') return; 2260 2261 // v2 widgets' "Shop the full menu" link â same gate, no item: outlet known 2262 // -> /order/{slug}; otherwise queue and open the journey. Handled here (not 2263 // in the widget) because this module owns location-gated routing. 2264 var menuLink = e.target.closest('[data-w2-menu-link]'); 2265 if (menuLink) { 2266 e.preventDefault(); 2267 _gatedSilverCatClick('', '', 0); 2268 return; 2269 } 2270 2271 var addBtn = e.target.closest('.add-ctrl__btn, .wla-best__btn'); 2272 var itemCard = addBtn ? null : e.target.closest('.item, .wla-best__card'); 2273 if (!addBtn && !itemCard) return; 2274 2275 // If the card itself is wrapped in <a>, the URL gate already covered it.
2276 var card = addBtn ? (addBtn.closest('.item') || addBtn.closest('.wla-best__card')) : itemCard; 2277 if (!card || card.tagName === 'A') return; 2278 2279 var itemSlug = card.getAttribute('data-item-slug') || ''; 2280 var slug = localStorage.getItem('slug'); 2281 var business = localStorage.getItem('business'); 2282 var hasOutletPick = slug && slug !== 'null' && business && business !== 'null'; 2283 2284 e.preventDefault(); 2285 if (hasOutletPick) { 2286 // Outlet known â jump straight to the menu (with the item preselected if we have a slug). 2287 var qs = itemSlug ? '?item_slug=' + encodeURIComponent(itemSlug) : ''; 2288 window.location.href = '/order/' + slug + qs; 2289 return; 2290 } 2291 2292 // No outlet yet â queue a sensible destination, then open the location flow. 2293 // Catalog URL words are brand-configurable (default products / product-detail). 2294 var _cfg = window.AppConfig || {}; 2295 var _listSlug = _cfg.catalogSlug || 'products'; 2296 var _detailSlug = _cfg.catalogDetailSlug || 'product-detail'; 2297 window._lpPendingNav = itemSlug ? '/' + _detailSlug + '/' + itemSlug : '/' + _listSlug; 2298 var ud2 = userData(); 2299 showLocationStep(ud2.token && ud2.contactMappingId ? 'savedAddresses' : 'location'); 2300 showOverlay(); 2301 }, true); 2302 2303 updateHeaderInfoBar(); 2304 2305 // Publish the real header height as --site-header-h so the location bar's 2306 // sticky `top` docks flush under it. Keeps the offset correct regardless of 2307 // logo size / nav padding (no magic numbers); re-synced on resize + font load. 2308 (function syncHeaderHeight() { 2309 function sync() { 2310 var h = document.querySelector('.site-header'); 2311 if (!h) return; 2312 var px = Math.round(h.getBoundingClientRect().height); 2313 if (px > 0) document.documentElement.style.setProperty('--site-header-h', px + 'px'); 2314 } 2315 sync(); 2316 window.addEventListener('resize', sync, { passive: true }); 2317 window.addEventListener('load', sync, { passive: true }); 2318 })(); 2319 2320 // Sticky scroll behavior â add `location-bar-scrolled` class on scroll, remove at top. 2321 (function attachStickyScroll() { 2322 var bar = document.getElementById('locationInfoBar'); 2323 if (!bar) return; 2324 var ticking = false; 2325 function onScroll() { 2326 if (ticking) return; 2327 ticking = true; 2328 window.requestAnimationFrame(function () { 2329 var y = window.pageYOffset || document.documentElement.scrollTop || 0; 2330 bar.classList.toggle('location-bar-scrolled', y > 8); 2331 ticking = false; 2332 }); 2333 } 2334 window.addEventListener('scroll', onScroll, { passive: true }); 2335 onScroll(); 2336 })(); 2337 2338 // Keep the bar in sync after the flow runs. The visibility/contents are otherwise 2339 // computed only once on load, so a selection made inside the modal (or in another tab) 2340 // wouldn't reflect until a full reload â and a cancelled "Change" could leave it stale. 2341 modalEl.addEventListener('hidden.bs.modal', updateHeaderInfoBar); 2342 // menu.js dispatches this when the order type changes in the SAME tab (its strip, 2343 // or the outlet default it adopts on load), which fires no `storage` event. The bar 2344 // carries its own .headerOrderType copy, so without this the mobile bar went stale 2345 // while the desktop chip updated. Subscribed HERE rather than widening headerV2's 2346 // selector: this function owns the bar's copy, and two writers for one element is 2347 // how they drift apart. 2348 document.addEventListener('ordertype:changed', updateHeaderInfoBar); 2349 window.addEventListener('storage', function (e) { 2350 if (!e.key || _LP_LOCATION_KEYS.indexOf(e.key) !== -1) updateHeaderInfoBar(); 2351 }); 2352 2353 // âââ Auto-open on first visit âââââââââââââââââââââââââââ 2354 // CI3 wla_home_20260507.js:2483-2512 â modal is suppressed only when the user has a 2355 // confirmed outlet tuple (slug + business). userLat/userLng alone don't count because 2356 // they're written before order-type selection completes. If a user does GPS but never 2357 // picks an order type, slug/business stay empty and the modal must reopen. 2358 function maybeAutoOpen() { 2359 if (!AppConfig.autoOpenLocationModal) return; 2360 var slug = localStorage.getItem('slug'); 2361 var business = localStorage.getItem('business'); 2362 var hasOutlet = slug && slug !== 'null' && business && business !== 'null';
2363 if (hasOutlet) return; 2364 // CI3 parity (wla_20260417.js:2134): don't stack the location modal on top of the 2365 // first-login welcome form. This Bootstrap modal's focus-trap would steal keystrokes 2366 // from the welcome form's inputs, leaving it visible but un-typeable. firstLoginProfile.js 2367 // runs just before this script, so the overlay is already shown when name is empty. 2368 // The user can reopen location selection from the header pill after finishing the form. 2369 var pp = document.getElementById('profilePromptModal'); 2370 if (pp && !pp.hasAttribute('hidden')) return; 2371 var ud = userData(); 2372 showLocationStep(ud.token ? 'savedAddresses' : 'location'); 2373 showOverlay(); 2374 } 2375 2376 if (document.readyState === 'loading') { 2377 document.addEventListener('DOMContentLoaded', maybeAutoOpen); 2378 } else { 2379 maybeAutoOpen(); 2380 } 2381})();
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.