PageSourceSearch

https://www.iwant2eat.com/js/geo-store.bymqbj5izk.js

js iwant2eat.com collected 2026-09-24 14:45:46 UTC 20,204 bytes, 407 lines download raw bytes

1// Automatic "Near me" store detection + keep-fresh bridge for the shopping
2// list. There is deliberately NO button and NO setting: detection runs silently
3// on page-open, on foreground (phone re-opened), and on the 10-minute tick.
4// init()'s second arg only reflects whether detecting can accomplish anything
5// (the household has items AND stores); the Account page hosts nothing but the
6// permission-recovery retry (permissionState/requestNow below). The component
7// only ever hears about a USABLE fix (OnGeoLocationAuto); every failure mode —
8// no permission, dismissed prompt, timeout, coarse fix — stays completely silent.
9window.iweGeo = (function () {
10  // In-memory once-per-page guard for the permission prompt. Primary defence is
11  // the localStorage flag below (survives across cold relaunches), but hardened
12  // browser configs (Safari "Block All Cookies" etc.) make storage setters
13  // THROW — without this flag those users would be re-prompted on every
14  // foreground and every 10-min tick, forever.
15  var asked = false;
16
17  // How good a fix has to be before we stop waiting. The server needs better
18  // than 500 m to name a store; 60 m is comfortably inside that and is what a
19  // phone's GPS settles at outdoors within a couple of seconds.
20  var GOOD_ENOUGH_M = 60;
21  // How long to keep listening for a better one. Long enough for GPS to take
22  // over from the network fix indoors, short enough that a store-detect on
23  // page-open doesn't hold the radio awake.
24  var BURST_MS = 8000;
25
26  // ONE fix, acquired properly.
27  //
28  // This used to be a single getCurrentPosition with maximumAge: 300000, and it
29  // is why store detection quietly never worked in a shop: the browser is free
30  // to answer a five-minute-old cached position, enableHighAccuracy does NOT
31  // force a fresh read when a cached one satisfies maximumAge, and the cached
32  // one is whatever coarse network fix the device happened to have. A real trip
33  // to a supermarket produced 4239 m twice — the same IP-derived position hours
34  // apart — which the server rightly refused as too coarse to name a store, so
35  // nothing was ever detected.
36  //
37  // watchPosition instead: the first callback is usually that same coarse
38  // network fix, and GPS then refines it over the next few seconds. We keep the
39  // BEST accuracy seen, stop as soon as it's good enough, and give up after
40  // BURST_MS with whatever we've got. maximumAge: 0 so the cache can't answer
41  // for us at all.
42  function locate(onFix, onFail) {
43    var best = null, watchId = null, timer = 0, done = false;
44
45    function finish() {
46      if (done) return;
47      done = true;
48      if (timer) { clearTimeout(timer); timer = 0; }
49      if (watchId !== null) {
50        try { navigator.geolocation.clearWatch(watchId); } catch (_) {}
51        watchId = null;
52      }
53      if (best) onFix(best); else if (onFail) onFail(null);
54    }
55
56    try {
57      watchId = navigator.geolocation.watchPosition(
58        function (pos) {
59          var acc = (pos.coords && pos.coords.accuracy) || 0;
60          // accuracy 0 means "unknown", not "perfect" — treat it as worst.
61          var score = acc > 0 ? acc : Number.MAX_VALUE;
62          if (!best || score < best.score) best = { pos: pos, score: score };
63          if (score <= GOOD_ENOUGH_M) finish();
64        },
65        function (err) {
66          // A late error after we already have a fix isn't a failure — a phone
67          // that got GPS and then lost it still told us where it was.
68          if (best) { finish(); return; }
69          if (done) return;
70          done = true;
71          if (timer) { clearTimeout(timer); timer = 0; }
72          if (watchId !== null) {
73            try { navigator.geolocation.clearWatch(watchId); } catch (_) {}
74            watchId = null;
75          }
76          if (onFail) onFail(err);
77        },
78        { enableHighAccuracy: true, timeout: BURST_MS, maximumAge: 0 }
79      );
80      timer = setTimeout(finish, BURST_MS);
81    } catch (_) {
82      if (onFail) onFail(null);
83    }
84  }
85
86  return {
87    // One detection attempt, gated so it never nags:
88    //   granted  → locate silently (every call).
89    //   denied   → do nothing, ever.
90    //   prompt   → ask AT MOST once per DEVICE (localStorage flag), so the
91    //              browser's native permission prompt is the one-time opt-in and
92    //              a dismissed/ignored prompt doesn't re-appear on every
93    //              foreground — including cold PWA relaunches, which iOS/Firefox
94    //              do aggressively and which would otherwise reset a
95    //              session-scoped guard and re-nag on every launch. A device that
96    //              dismissed can still opt in later via the Account page retry.
97    // Old browsers without the Permissions API can't distinguish these states:
98    // there we attempt freely only after a past success (localStorage iweGeoOk —
99    // effectively "granted"), otherwise once per device.
100    detectAuto: function (dotnetRef) {
101      try {
102        if (!navigator.geolocation) return;
103
104        var attempt = function () {
105          locate(
106            function (best) {
107              var pos = best.pos;
108              try { localStorage.iweGeoOk = '1'; } catch (_) {}
109              // .catch: the component may have been disposed (nav away) during
110              // the async geolocation gap → drop the late callback quietly.
111              dotnetRef.invokeMethodAsync(
112                'OnGeoLocationAuto', pos.coords.latitude, pos.coords.longitude, pos.coords.accuracy || 0)
113                .catch(function () { /* torn down mid-detect */ });
114            },
115            function (err) {
116              // Permanently denied → forget the past-success marker so the
117              // no-Permissions-API fallback stops re-attempting next sessions.
118              if (err && err.code === 1) { try { delete localStorage.iweGeoOk; } catch (_) {} }
119            }
120          );
121        };
122        var attemptOnceEver = function () {
123          if (asked) return;
124          asked = true;   // page-lifetime guard — holds even when storage throws
125          try {
126            // localStorage (not sessionStorage): the guard must survive cold PWA
127            // relaunches, or the auto-prompt re-fires every time the OS kills and
128            // reopens the app. At most one auto-prompt per device, ever.
129            if (localStorage.iweGeoAsked === '1') return;
130            localStorage.iweGeoAsked = '1';
131          } catch (_) { /* storage blocked → the in-memory flag still limits us */ }
132          attempt();
133        };
134
135        if (navigator.permissions && navigator.permissions.query) {
136          navigator.permissions.query({ name: 'geolocation' }).then(function (status) {
137            if (status.state === 'granted') attempt();
138            else if (status.state === 'prompt') attemptOnceEver();
139            // 'denied' → silent, never call.
140          }).catch(attemptOnceEver);          // Permissions API quirk → degrade to once-per-device
141        } else if (localStorage.iweGeoOk === '1') {
142          attempt();                          // effectively granted (a past success)
143        } else {
144          attemptOnceEver();
145        }
146      } catch (_) { /* swallow — detection must never break the page */ }
147    },
148
149    // For the Account page's location-recovery section: report the current
150    // geolocation permission state ('granted'/'prompt'/'denied'/'unsupported').
151    // Where the Permissions API is missing (older Safari), fall back to the same
152    // past-success marker detectAuto uses — otherwise a working, effectively-
153    // granted device would see the "fix your location" section on every visit.
154    permissionState: function () {
155      function fallback() {
156        try { return localStorage.iweGeoOk === '1' ? 'granted' : 'prompt'; }
157        catch (_) { return 'prompt'; }
158      }
159      try {
160        if (!navigator.geolocation) return Promise.resolve('unsupported');
161        if (!navigator.permissions || !navigator.permissions.query) return Promise.resolve(fallback());
162        return navigator.permissions.query({ name: 'geolocation' })
163          .then(function (s) { return s.state; })
164          .catch(function () { return fallback(); });
165      } catch (_) { return Promise.resolve('unsupported'); }
166    },
167
168    // Explicit user-gesture retry (the Account-page button): attempt a fix NOW.
169    // In the 'prompt' state this raises the browser's native permission dialog;
170    // after a hard "Block" the browser refuses without prompting and we resolve
171    // 'denied' so the UI can point at browser settings. Success marks the same
172    // flags detectAuto uses, so the shopping list picks it up seamlessly.
173    requestNow: function () {
174      return new Promise(function (resolve) {
175        try {
176          if (!navigator.geolocation) { resolve('unsupported'); return; }
177          try { localStorage.iweGeoAsked = '1'; } catch (_) {}
178          // Same burst as the silent path — a "retry" that accepted a cached
179          // 4 km fix would report success while leaving detection just as
180          // broken as before.
181          locate(
182            function () {
183              try { localStorage.iweGeoOk = '1'; } catch (_) {}
184              resolve('granted');
185            },
186            function (err) {
187              if (err && err.code === 1) { resolve('denied'); return; }
188              // Timeout / position-unavailable is NOT "blocked in the browser":
189              // the user may have just ACCEPTED the prompt and simply gotten no
190              // fix (indoors, OS location services off). Re-check the actual
191              // permission so the UI doesn't show the wrong "blocked" help.
192              if (navigator.permissions && navigator.permissions.query) {
193                navigator.permissions.query({ name: 'geolocation' }).then(function (s) {
194                  if (s.state === 'granted') { try { localStorage.iweGeoOk = '1'; } catch (_) {} resolve('granted'); }
195                  else resolve(s.state === 'denied' ? 'denied' : 'error');
196                }).catch(function () { resolve('error'); });
197              } else { resolve('error'); }
198            }
199          );
200        } catch (_) { resolve('error'); }
201      });
202    }
203  };
204})();
205
206// Keep-fresh bridge. The Blazor component registers its dotnet ref (plus
207// whether store detection is enabled) on first render; we then refresh the
208// list — and re-detect the store — when the app returns to the foreground and
209// every 10 min while it stays open. The live HouseholdListNotifier already
210// pushes changes while the SignalR circuit is connected; this covers the gap
211// after a background/lock, where the circuit gets evicted and the page would
212// otherwise sit stale.
213window.iweHousehold = (function () {
214  var ref = null, timer = 0, lastRefresh = 0, detectOn = false;
215
216  function maybeDetect() { if (ref && detectOn) window.iweGeo.detectAuto(ref); }
217
218  function refresh() {
219    if (!ref) return;
220    var now = Date.now();
221    if (now - lastRefresh < 3000) return;   // debounce the visibilitychange/pageshow double-fire
222    lastRefresh = now;
223    // If the invoke rejects the circuit is down — do NOTHING and let Blazor's
224    // built-in auto-reconnect restore it. We deliberately don't location.reload()
225    // here: on a deploy/eviction navigator.onLine is still true, so a reload would
226    // storm (reload → no circuit → reload …) and fight auto-reconnect. A genuine
227    // page reload (Blazor gave up, or iOS killed the PWA) re-runs detection via
228    // OnAfterRenderAsync anyway.
229    ref.invokeMethodAsync('RefreshFromClient')
230      .then(maybeDetect)
231      .catch(function () { /* circuit down → let Blazor auto-reconnect handle it */ });
232  }
233
234  // Only visibilitychange + pageshow — the real "app came to the foreground"
235  // signals. 'focus' fires far too eagerly on desktop (every alt-tab) and would
236  // churn the list / re-detect for no benefit.
237  function onVisible() { if (!document.hidden) refresh(); }
238
239  function teardown() {
240    if (timer) { clearInterval(timer); timer = 0; }
241    document.removeEventListener('visibilitychange', onVisible);
242    window.removeEventListener('pageshow', onVisible);
243    ref = null;
244    detectOn = false;
245  }
246
247  return {
248    init: function (dotnetRef, detectEnabled) {
249      teardown();                       // idempotent — drop any prior registration
250      ref = dotnetRef;
251      detectOn = !!detectEnabled;
252      document.addEventListener('visibilitychange', onVisible);
253      window.addEventListener('pageshow', onVisible);
254      timer = setInterval(function () {
255        if (document.hidden || !ref) return;   // don't poke a backgrounded circuit
256        ref.invokeMethodAsync('RefreshFromClient').then(maybeDetect).catch(function () {});
257      }, 600000);                       // 10 min
258      maybeDetect();                    // initial detection on open
259    },
260    dispose: teardown,
261    // Diagnostics only — lets a live page prove the bridge actually wired
262    // (a silent-failure regression here is invisible in normal use).
263    wired: function () { return !!ref; },
264    detecting: function () { return detectOn; }
265  };
266})();
267
268// Viewer-local timestamp formatting. Blazor Server renders DateTimes in the
269// SERVER's timezone and culture (prod: UTC + invariant) — hours off for the
270// user and always US-shaped. Any element carrying data-utc (an ISO-8601 UTC
271// instant) gets rewritten here with the browser's own TZ + locale. Idempotent:
272// once formatted, the marker attribute flips so re-runs skip it; Blazor
273// re-creates elements on re-render (fresh, unformatted), so the component
274// calls format() after every render.
275// Floating scroll-to-top / scroll-to-bottom arrows. Any page that renders the
276// #scroll-nav-up / #scroll-nav-down buttons calls this once after render; the
277// buttons fade in only when the viewport isn't already near the top/bottom.
278// Idempotent + global: the handler re-queries by id on every fire (so Blazor's
279// per-navigation DOM swap can't leave it pointing at a detached no
279de) and any
280// previous installation is removed first so repeated navigations don't stack.
281window.iweScrollNav = function () {
282  try {
283    if (window.__scrollNavHandler) {
284      window.removeEventListener('scroll', window.__scrollNavHandler);
285      window.removeEventListener('resize', window.__scrollNavHandler);
286    }
287    var update = function () {
288      var up = document.getElementById('scroll-nav-up');
289      var down = document.getElementById('scroll-nav-down');
290      if (!up || !down) return;
291      var y = window.scrollY || document.documentElement.scrollTop;
292      var max = (document.documentElement.scrollHeight || document.body.scrollHeight) - window.innerHeight;
293      up.classList.toggle('visible', y > 200);
294      down.classList.toggle('visible', y < max - 200);
295    };
296    window.__scrollNavHandler = update;
297    window.addEventListener('scroll', update, { passive: true });
298    window.addEventListener('resize', update);
299    update();
300  } catch (_) { /* swallow */ }
301};
302
303window.iweTime = {
304  // Minutes to ADD to local time to get UTC (JS convention: EDT → +240).
305  // The server uses this to compute the viewer's local midnight so the
306  // "recently bought" strip clears at local midnight, not on a rolling 24h.
307  tzOffsetMinutes: function () {
308    try { return new Date().getTimezoneOffset(); } catch (_) { return 0; }
309  },
310  // Coarse "today / yesterday / N days ago" phrase, in the viewer's LOCAL
311  // calendar days and locale. Auto-localized via Intl.RelativeTimeFormat.
312  relDay: function (d) {
313    var now = new Date();
314    var a = new Date(now.getFullYear(), now.getMonth(), now.getDate());
315    var b = new Date(d.getFullYear(), d.getMonth(), d.getDate());
316    var days = Math.round((a - b) / 86400000);
317    try {
318      return new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }).format(-days, 'day');
319    } catch (_) {
320      return days <= 0 ? 'today' : days === 1 ? 'yesterday' : days + ' days ago';
321    }
322  },
323  // Fine-grained "just now / 5 minutes ago / 2 hours ago" phrase. Distinct
324  // from relDay above, which is deliberately day-granular ("yesterday") and is
325  // what FriendsPanel wants — this one is for something that just happened and
326  // whose age matters in minutes.
327  relLive: function (d) {
328    var secs = Math.round((Date.now() - d.getTime()) / 1000);
329    // Clock skew or a stamp a second in the future: don't say "in 0 seconds".
330    if (secs < 45) return 'just now';
331    var units = [
332      ['minute', 60], ['hour', 3600], ['day', 86400],
333      ['week', 604800], ['month', 2629800], ['year', 31557600],
334    ];
335    // Pick the largest unit the age fills at least once.
336    var unit = 'minute', size = 60;
337    for (var i = 0; i < units.length; i++) {
338      if (secs >= units[i][1]) { unit = units[i][0]; size = units[i][1]; }
339    }
340    var n = Math.round(secs / size);
341    try {
342      return new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' }).format(-n, unit);
343    } catch (_) {
344      return n + ' ' + unit + (n === 1 ? '' : 's') + ' ago';
345    }
346  },
347  format: function () {
348    try {
349      document.querySelectorAll('[data-utc]').forEach(function (el) {
350        try {
351          var src = el.getAttribute('data-utc');
352          // Re-format whenever the source value changes — not just the first
353          // time. Blazor reuses an element and swaps its data-utc in place
354          // (e.g. a Friends invite after a resend), so a permanent "done" flag
355          // would leave the old localized text stuck until a full reload.
356          // data-utc-done records the exact value we last formatted for.
357          // A LIVE relative stamp must re-render on every pass even though
358          // its source instant never changes — the whole point is that the
359          // text ages ("just now" -> "5 minutes ago"). The done-guard below
360          // exists to skip unchanged ABSOLUTE values, so live ones jump it.
361          var live = el.hasAttribute('data-rel-live');
362          if (!live && el.getAttribute('data-utc-done') === src) return;
363          var d = new Date(src);
364          if (isNaN(d)) return;
365          if (live) {
366            el.textContent = window.iweTime.relLive(d);
367          } else if (el.hasAttribute('data-rel')) {
368            // data-rel → coarse relative phrase ("3 days ago") instead of a date.
369            el.textContent = window.iweTime.relDay(d);
370          } else {
371            // data-time24 → force 24-hour time (no AM/PM), e.g. "7/9/26, 19:00".
372            var opts = el.hasAttribute('data-time24')
373              ? { year: '2-digit', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }
374              : { dateStyle: 'short', timeStyle: 'short' };
375            el.textContent = d.toLocaleString(undefined, opts);
376          }
377          el.setAttribute('data-utc-done', src);
378        } catch (_) { /* leave the server-rendered fallback text */ }
379      });
380    } catch (_) { /* swallow */ }
381  }
382};
383
384// Keep [data-rel-live] stamps honest without a server round-trip. Blazor only
385// re-renders on an interaction, so a "just now" written at page load would
386// still read "just now" an hour later; this re-runs the formatter on a timer.
387// 30s is the coarsest interval that still flips "just now" (< 45s) promptly.
388// Installed once — the guard survives repeated script evaluation — and it only
389// walks the DOM when such an element is actually present, so pages without one
390// pay a querySelector every 30 seconds and nothing else.
391(function () {
392  if (window.__iweRelLiveTimer) return;
393  window.__iweRelLiveTimer = setInterval(function () {
394    try {
395      if (!document.querySelector('[data-rel-live]')) return;
396      window.iweTime.format();
397    } catch (_) { /* swallow */ }
398  }, 30000);
399  // A backgrounded tab throttles timers, so the stamp can be minutes stale by
400  // the time it is looked at again. Refresh the moment it becomes visible.
401  document.addEventListener('visibilitychange', function () {
402    if (document.visibilityState !== 'visible') return;
403    try {
404      if (document.querySelector('[data-rel-live]')) window.iweTime.format();
405    } catch (_) { /* swallow */ }
406  });
407})();

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.