Skip to content

feat: add Geolocation API#23527

Merged
Artur- merged 35 commits into
mainfrom
feature/geo
Apr 24, 2026
Merged

feat: add Geolocation API#23527
Artur- merged 35 commits into
mainfrom
feature/geo

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented Feb 15, 2026

Summary

  • Adds a per-UI Geolocation facade (UI.getGeolocation()) that wraps the browser's Geolocation API with a one-shot get(...) request, a reactive track(Component) session, and an availabilitySignal() reporting capability/permission state.
  • Results use sealed types designed for exhaustive pattern matching: GeolocationOutcome (GeolocationPosition | GeolocationError) for one-shot reads, and GeolocationResult (adds GeolocationPending) for the tracker's signal before the first fix arrives.
  • Availability is delivered synchronously — seeded in the bootstrap handshake and kept in sync via a vaadin-geolocation-availability-change DOM event — so application code can read it (or bind to it) without an extra round-trip.

Details

Geolocation.get(callback) and Geolocation.get(options, callback) enqueue a single position request; the callback receives a GeolocationOutcome on the UI thread.

Geolocation.track(Component) / track(Component, GeolocationOptions) returns a GeolocationTracker:

  • valueSignal()Signal<GeolocationResult> carrying GeolocationPending until the first fix, then successive GeolocationPosition / GeolocationError values.
  • activeSignal()Signal<Boolean> for binding a start/stop button without tracking a separate flag.
  • stop() / resume() — cancel or resume the browser watch; the tracker handle is reusable and bound effects keep working.
  • The watch is cancelled automatically when the owning component detaches. Javadoc documents the cross-browser caveat that a watch goes silent if the user revokes and re-grants permission, and recommends stop() + resume() (or driving it off availabilitySignal()) as recovery.

GeolocationOptions is a record with a serializable builder, Duration overloads, and validation that rejects negative timeout / maximumAge.

GeolocationError.errorCode() returns a GeolocationErrorCode enum (PERMISSION_DENIED, POSITION_UNAVAILABLE, TIMEOUT, UNKNOWN) so switches are exhaustive without a null arm.

availabilitySignal() yields a GeolocationAvailability enum (GRANTED / DENIED / PROMPT / UNKNOWN / UNSUPPORTED). Javadoc spells out the reliability caveats (Safari always reports UNKNOWN, Firefox does not always propagate settings changes, a small propagation delay on Chromium) and recommends get(...) in the callback for
critical paths.

The client bridge lives in flow-client/src/main/frontend/Geolocation.ts, loaded as a side-effect import from Flow.ts (the entry point Vite actually bundles). collectBrowserDetails() awaits queryAvailability() so the initial value is present in the first server request. k

The package is @NullMarked (JSpecify) with @Nullable on the genuinely optional spots (options, boxed coordinate fields, internal wire records). Read-only signal wrappers are cached so callers observe stable identities. Accompanied by unit tests in GeolocationTest and an integration test (GeolocationIT + GeolocationView)
covering granted/denied/error paths and the "no updates after stop()" invariant.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 15, 2026

Test Results

 1 393 files  + 2   1 393 suites  +2   1h 15m 37s ⏱️ - 1m 1s
10 045 tests +36   9 975 ✅ +36  70 💤 ±0  0 ❌ ±0 
10 520 runs  +36  10 441 ✅ +36  79 💤 ±0  0 ❌ ±0 

Results for commit 6757e2b. ± Comparison against base commit 087c129.

♻️ This comment has been updated with latest results.

Comment thread flow-server/src/main/java/com/vaadin/flow/component/geolocation/Geolocation.java Outdated
@Artur- Artur- marked this pull request as ready for review February 21, 2026 11:09
@sonarqubecloud
Copy link
Copy Markdown

Comment thread flow-server/src/main/resources/META-INF/frontend/geolocation.js Outdated
Dustin4444

This comment was marked as spam.

Artur- added a commit to vaadin/flow-components that referenced this pull request Apr 11, 2026
@mshabarov mshabarov moved this from 🔎Iteration reviews to ⚒️ In progress in Vaadin Flow | Hilla | Kits ongoing work Apr 15, 2026
@mshabarov mshabarov moved this from ⚒️ In progress to 🟢Ready to Go in Vaadin Flow | Hilla | Kits ongoing work Apr 15, 2026
Artur- added 10 commits April 21, 2026 16:50
Provides access to the browser's Geolocation API with two usage modes:

- get(): one-shot position request with success/error callbacks
- track(): continuous tracking via a reactive Signal tied to a
  component's lifecycle, using a GeolocationState sealed type
  (Pending | GeolocationPosition | GeolocationError) that enables
  exhaustive pattern matching

Includes records for coordinates, position, error, and options,
a JS module that bridges navigator.geolocation to Vaadin's event
system, and unit + integration tests.
Convert the plain JS geolocation module to TypeScript with typed
interfaces matching the Java records, and move it from flow-server
to flow-client where other core client infrastructure lives.

The module is loaded via flow-client's index.ts entry point instead
of a @jsmodule annotation, following the same pattern as Flow.ts.
Signal.get() requires a reactive context, which unit tests don't
have. Use peek() to read signal values without dependency tracking,
matching the pattern used by other signal tests in the codebase.
Import Geolocation from Flow.ts instead of index.ts, since Flow.ts
is what Vite actually bundles at runtime. Add empty export to make
TypeScript emit it as an ES module.

Fix track IT test race condition by listening for the DOM event
directly instead of polling the signal after a single JS round-trip.
Use peek() in view code since it runs outside a reactive context.

Add debug div to GeolocationView to report geolocation API status.
Extends the Geolocation API with stop(), isSupported(), queryPermission(),
the GeolocationPermission and GeolocationErrorCode enums, and a Duration-
accepting builder on GeolocationOptions. Javadoc across the package is
rewritten for Java developers who do not read the underlying JS API, and
the TypeScript helper drops fallbacks that are only relevant to browsers
outside the supported set (Safari < 17 Apple-epoch timestamps, defensive
feature checks).
The Vaadin serialization tests scan all inner classes and require them
to be Serializable. Builder was missing the marker interface.
Replaces the static Geolocation.get/.track entry points with an
instance-based facade reached via ui.getGeolocation(). The one-shot
get() now takes a single SerializableConsumer<GeolocationResult>
instead of separate success and error callbacks, letting applications
pattern-match on the result. Continuous tracking returns a new
GeolocationTracker handle whose value() exposes Signal<GeolocationResult>
and whose stop() cancels the watch. GeolocationState is renamed to
GeolocationResult. isSupported and queryPermission move to instance
methods on the same facade; the static API is removed.
Collapses the two capability checks into a single queryAvailability()
call returning a GeolocationAvailability enum (GRANTED / DENIED / PROMPT
/ UNKNOWN / UNSUPPORTED). One JS round-trip covers both the "is the API
usable here" and "what permission state has the user granted" questions,
and applications pick the branch that matches the combined answer.

GeolocationPermission is removed; isSupported() and queryPermission()
disappear. Also tightens a few Javadoc paragraphs that over-promised
about UI-thread / ui.access() guarantees.
Moves GeolocationAvailability off an async callback and into a
synchronous getGeolocation().getAvailability() backed by
ExtendedClientDetails. The initial value ships with the browser-details
init handshake (v-ga); get()/track() responses refresh it inline via
their JSON payload; a vaadin-geolocation-availability-change DOM event,
dispatched by the client's permissionchange listener and received by a
DOM listener registered in Geolocation's constructor, keeps the cached
value current when the user flips the site permission mid-session. The
Geolocation instance is now built in UI's constructor body after
internals are set, so the listener can register upfront without lazy
guards.

Also drops GeolocationResult.Pending: the sealed interface now permits
only Position and Error. The tracker's signal starts null and callers
pattern-match with case null for the waiting state, removing the dead
case arm from every get() switch.

Flow.ts collectBrowserDetails is now async and awaits
window.Vaadin.Flow.geolocation.queryAvailability() so the value is
ready in the very first request to the server.
Flow.ts: hoists the await for collectBrowserDetails() out of the Promise
body (flowInitUi is already async); drops the defensive
typeof-check/try-catch around window.Vaadin.Flow.geolocation.queryAvailability
since Geolocation.ts is imported as a side-effect module.

Geolocation.ts: inlines deriveSupported into its only caller; replaces
the `(window as any).Vaadin = (window as any).Vaadin || {}` bootstrap
with ES2021 ??= assignment behind a local $wnd alias; renames
applyAvailabilityFromResult to getAndCacheAvailabilityFromResult to
reflect that it both caches and returns the value; get() callers now
use the function's return value directly instead of reading the
module-level cache after a side-effectful call.
Artur- added 2 commits April 23, 2026 12:45
A position event already in-flight when the Stop click reaches the
server can still produce a new trackResult div after stopResult has
been appended to the DOM. Pause 100 ms before snapshotting the count
so that trailing in-flight event has landed first; without this the
"no new divs after stop()" assertion is flaky.
`asReadonly()` allocates a fresh lambda-backed wrapper on every call,
so callers got a different instance each time — behaviorally
equivalent (all delegate to the same underlying ValueSignal) but
identity-unstable and wasteful. Cache one wrapper per signal on the
tracker and the Geolocation facade and return that.
@mcollovati
Copy link
Copy Markdown
Collaborator

mcollovati commented Apr 23, 2026

I get an unexpected behavior if I start tracking and then revoke and grant again the permission after a couple of positions are received: in this case, the browser stops sending position updates.
If I call GeolocationTraker.stop() and GeolocationTraker.resume() then it starts to work again. But I guess this is because a new watch is performed.

I don't know if this is the default browser behavior, if my testing is incorrect, or if it is a bug somewhere in the implementation.

EDIT: the same behavior is also with plain JavaScript. @Artur- I guess this needs to be documented somehow.

mcollovati and others added 5 commits April 23, 2026 16:17
maximumAge cannot be negative anymore, use 9999 to trigger the error
…pattern

VaadinSession.localeSignal(), windowSizeSignal, and validationStatusSignal
all use the *Signal() naming when exposing a Signal<T> accessor. Align
the three geolocation accessors:

- GeolocationTracker.value()        -> valueSignal()
- GeolocationTracker.active()       -> activeSignal()
- Geolocation.availability()        -> availabilitySignal()

Javadoc examples, cross-references and callers in tests and the IT view
are updated to match.
@github-actions github-actions Bot added +0.1.0 and removed +1.0.0 labels Apr 23, 2026
Artur- added 2 commits April 23, 2026 15:34
UIInternals#getGeolocationAvailabilitySignal still linked to
Geolocation#availability(), which was renamed to availabilitySignal()
in the *Signal() naming pass. Unit-tests job 3 failed because the
javadoc goal is fail-on-error.
@Artur-
Copy link
Copy Markdown
Member Author

Artur- commented Apr 24, 2026

Something here we need to deal with still for an initial version?

Browsers silently stop delivering position updates to an active
watchPosition() if the user revokes permission and then grants it
again — the existing watch is effectively dead and a fresh
watchPosition() call is required. This is W3C-spec behavior across
browsers, not Flow-specific, but applications that don't know to
stop/resume the tracker see updates quietly stop.

Document the caveat on Geolocation.track(Component) with the
recommended recovery pattern (stop() + resume()) and a pointer to
availabilitySignal() for apps that want to automate it.
@Artur-
Copy link
Copy Markdown
Member Author

Artur- commented Apr 24, 2026

Added javadoc about revoking access. Hopefully the workaround also works

@sonarqubecloud
Copy link
Copy Markdown

@mcollovati
Copy link
Copy Markdown
Collaborator

Tested the latest changes and the permission revoke/grant workaround, and it works well.

@Artur- Artur- enabled auto-merge April 24, 2026 08:21
@Artur- Artur- dismissed platosha’s stale review April 24, 2026 08:27

The comments have been resolved

@Artur- Artur- added this pull request to the merge queue Apr 24, 2026
Merged via the queue into main with commit 498c4c4 Apr 24, 2026
31 checks passed
@Artur- Artur- deleted the feature/geo branch April 24, 2026 08:41
@github-project-automation github-project-automation Bot moved this from ⚒️ In progress to Done in Vaadin Flow | Hilla | Kits ongoing work Apr 24, 2026
@mshabarov
Copy link
Copy Markdown
Contributor

Related-to vaadin/platform#8758

@vaadin-bot
Copy link
Copy Markdown
Collaborator

This ticket/PR has been released with Vaadin 25.2.0-alpha4.

Artur- added a commit to vaadin/docs that referenced this pull request Apr 29, 2026
Update the article to match the final API shipped in vaadin/flow#23527:
the per-UI facade obtained via UI.getGeolocation(), sealed
GeolocationOutcome / GeolocationResult types for pattern matching,
GeolocationTracker with valueSignal/activeSignal/stop/resume, the
GeolocationOptions builder with Duration overloads, the
availabilitySignal with its reliability caveats, and the
GeolocationErrorCode enum.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

8 participants