Fix the hero's "Use my location" doing nothing (#179)
The button was dead on plain HTTP and failed silently everywhere.
Browsers only expose geolocation on secure origins, but `navigator.geolocation`
still EXISTS on http:// — getCurrentPosition just fails with a permission error
("Only secure origins are allowed"). The guard was `if (!navigator.geolocation)
return;`, which passes on http://, so the call went ahead, errored, and landed
in an empty error callback. Nothing happened at all.
`make lan-run` serves plain HTTP on 0.0.0.0:8137 so phones can reach it, so the
button could never work in the normal dev-and-phone workflow, and gave no hint
why. It would have worked on thermograph.org, which is HTTPS.
- Gate on `window.isSecureContext`, not just the API's presence. Where the
browser will refuse, hide the locate button and promote "Pick on the map" to
primary rather than offering a control that cannot work.
- Report failures: a declined prompt, a timeout, and an unavailable fix each get
their own message in an aria-live slot. A silent failure is indistinguishable
from a broken button, which is exactly how this went unnoticed.
- Show a "Locating…" state while the fix is pending; it can take seconds.
To exercise geolocation against the LAN dev server, serve it over TLS:
`make lan-run TLS=1` (self-signed cert into certs/).
Verified by driving the button in a real browser across all three cases:
insecure origin (button hidden, map promoted), permission granted (hero
re-points at the located place), permission denied (message shown, button
restored).
This commit is contained in:
parent
c4ca6b38ea
commit
487c1a6ee4
2 changed files with 52 additions and 5 deletions
|
|
@ -55,17 +55,63 @@ initFindButton(findBtn, "Find a location",
|
||||||
// shifts nothing on the page.
|
// shifts nothing on the page.
|
||||||
const hero = document.getElementById("hero");
|
const hero = document.getElementById("hero");
|
||||||
|
|
||||||
document.getElementById("hero-locate")?.addEventListener("click", () => {
|
const heroLocate = document.getElementById("hero-locate");
|
||||||
|
const heroPick = document.getElementById("hero-pick");
|
||||||
|
const heroMsg = document.getElementById("hero-locate-msg");
|
||||||
|
|
||||||
|
function locateMsg(text) {
|
||||||
|
if (!heroMsg) return;
|
||||||
|
heroMsg.textContent = text || "";
|
||||||
|
heroMsg.hidden = !text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Browsers expose geolocation ONLY on secure origins, but `navigator.geolocation`
|
||||||
|
// still exists on plain http:// — getCurrentPosition just fails with a permission
|
||||||
|
// error. So feature-testing the object is not enough: without the isSecureContext
|
||||||
|
// check the button is a dead control on the plain-HTTP LAN dev server (and on any
|
||||||
|
// phone reaching it by IP), which is exactly how it shipped broken.
|
||||||
|
// Rather than offer a button that cannot work, promote the map picker instead.
|
||||||
|
const canLocate = !!navigator.geolocation && window.isSecureContext;
|
||||||
|
if (heroLocate && !canLocate) {
|
||||||
|
heroLocate.hidden = true;
|
||||||
|
heroPick?.classList.replace("btn-ghost", "btn-primary");
|
||||||
|
}
|
||||||
|
|
||||||
|
heroLocate?.addEventListener("click", () => {
|
||||||
track("home.locate");
|
track("home.locate");
|
||||||
if (!navigator.geolocation) return;
|
const label = heroLocate.textContent;
|
||||||
|
const restore = () => {
|
||||||
|
heroLocate.disabled = false;
|
||||||
|
heroLocate.textContent = label;
|
||||||
|
};
|
||||||
|
// Geolocation can take seconds (or hang until the timeout), so say so —
|
||||||
|
// a button that looks inert is indistinguishable from a broken one.
|
||||||
|
heroLocate.disabled = true;
|
||||||
|
heroLocate.textContent = "Locating…";
|
||||||
|
locateMsg("");
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(pos) => selectLocation(pos.coords.latitude, pos.coords.longitude),
|
(pos) => {
|
||||||
() => { /* declined or unavailable: the map picker is still right there */ },
|
restore();
|
||||||
|
selectLocation(pos.coords.latitude, pos.coords.longitude);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
restore();
|
||||||
|
// Always say something: a declined prompt or a timeout must not look
|
||||||
|
// like the button did nothing.
|
||||||
|
locateMsg(
|
||||||
|
err.code === err.PERMISSION_DENIED
|
||||||
|
? "Location permission was declined — pick a spot on the map instead."
|
||||||
|
: err.code === err.TIMEOUT
|
||||||
|
? "That took too long. Try again, or pick a spot on the map."
|
||||||
|
: "Your location isn't available right now — pick a spot on the map instead.",
|
||||||
|
);
|
||||||
|
},
|
||||||
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 600000 },
|
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 600000 },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("hero-pick")?.addEventListener("click", () => findBtn.click());
|
heroPick?.addEventListener("click", () => { locateMsg(""); findBtn.click(); });
|
||||||
|
|
||||||
/** Re-point the hero card at the place the visitor actually chose. */
|
/** Re-point the hero card at the place the visitor actually chose. */
|
||||||
function updateHero(data) {
|
function updateHero(data) {
|
||||||
|
|
|
||||||
|
|
@ -1802,6 +1802,7 @@ details.hub-country > summary:hover { color: var(--accent); }
|
||||||
.btn-primary { background: var(--accent); color: #1a1206; border: 1px solid var(--accent); }
|
.btn-primary { background: var(--accent); color: #1a1206; border: 1px solid var(--accent); }
|
||||||
.btn-ghost { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
|
.btn-ghost { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
|
||||||
.btn-ghost:hover, .btn-primary:hover { border-color: var(--accent); }
|
.btn-ghost:hover, .btn-primary:hover { border-color: var(--accent); }
|
||||||
|
.hero-locate-msg { margin: 0; font-size: 13px; color: var(--muted); max-width: 42ch; }
|
||||||
.hero-jump { margin: 0; font-size: 14px; }
|
.hero-jump { margin: 0; font-size: 14px; }
|
||||||
.hero-jump a { color: var(--muted); text-decoration: none; }
|
.hero-jump a { color: var(--muted); text-decoration: none; }
|
||||||
.hero-jump a:hover { color: var(--accent); }
|
.hero-jump a:hover { color: var(--accent); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue